From afab979733fa8c2b4234abe70ccbb55965187857 Mon Sep 17 00:00:00 2001 From: Martin Vladic Date: Fri, 15 Nov 2019 09:38:58 +0100 Subject: [PATCH] user switch and jpeg screenshot --- CMakeLists.txt | 12 + modular-psu-firmware.eez-project | 3 + src/eez/gui/action_impl.cpp | 74 ++- src/eez/gui/app_context.cpp | 13 + src/eez/gui/app_context.h | 17 +- src/eez/gui/dialogs.cpp | 6 +- src/eez/gui/event.cpp | 14 + src/eez/gui/gui.h | 4 + src/eez/gui/page.cpp | 127 ++-- src/eez/gui/page.h | 8 +- src/eez/gui/widget.cpp | 11 +- src/eez/libs/image/jpeg_encode.cpp | 50 ++ src/eez/libs/image/jpeg_encode.h | 21 + src/eez/libs/image/toojpeg.cpp | 572 ++++++++++++++++++ src/eez/libs/image/toojpeg.h | 57 ++ src/eez/modules/mcu/button.cpp | 25 +- src/eez/modules/mcu/button.h | 7 +- src/eez/modules/mcu/display.h | 4 +- src/eez/modules/mcu/encoder.cpp | 4 +- src/eez/modules/mcu/simulator/display.cpp | 65 +- src/eez/modules/mcu/stm32/display.cpp | 88 ++- src/eez/modules/psu/event_queue.cpp | 7 +- src/eez/modules/psu/event_queue.h | 3 +- src/eez/modules/psu/gui/data.cpp | 30 +- src/eez/modules/psu/gui/data.h | 7 +- src/eez/modules/psu/gui/psu.cpp | 6 +- src/eez/modules/psu/io_pins.cpp | 12 +- src/eez/modules/psu/io_pins.h | 3 + src/eez/modules/psu/persist_conf.cpp | 30 +- src/eez/modules/psu/persist_conf.h | 17 + src/eez/modules/psu/scpi/display.cpp | 69 +-- src/eez/platform/simulator/front_panel.cpp | 7 + src/eez/platform/simulator/front_panel.h | 6 +- src/eez/platform/stm32/defines.h | 16 +- src/eez/scpi/scpi.cpp | 63 +- src/eez/scpi/scpi.h | 1 + src/eez/scpi/scpi_user_config.h | 1 + src/third_party/stm32_r1b5/.cproject | 41 ++ src/third_party/stm32_r1b5/.mxproject | 4 +- .../stm32_r1b5/Inc/FreeRTOSConfig.h | 2 +- src/third_party/stm32_r1b5/Inc/ffconf.h | 2 +- src/third_party/stm32_r1b5/h25005.ioc | 6 +- .../stm32_r1b5/h25005_r1b5.elf.launch | 2 +- src/third_party/stm32_r1b5_cubeide/.cproject | 12 + .../stm32_r1b5_cubeide/.cproject_org | 24 + 45 files changed, 1295 insertions(+), 258 deletions(-) create mode 100644 src/eez/libs/image/jpeg_encode.cpp create mode 100644 src/eez/libs/image/jpeg_encode.h create mode 100644 src/eez/libs/image/toojpeg.cpp create mode 100644 src/eez/libs/image/toojpeg.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cd1bed64b..24e1dc84a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -358,6 +358,18 @@ set(header_eez_libs_sd_fat list (APPEND header_files ${header_eez_libs_sd_fat}) source_group("eez\\libs\\sd_fat" FILES ${src_eez_libs_sd_fat} ${header_eez_libs_sd_fat}) +set(src_eez_libs_image + src/eez/libs/image/jpeg_encode.cpp + src/eez/libs/image/toojpeg.cpp +) +list (APPEND src_files ${src_eez_libs_image}) +set(header_eez_libs_image + src/eez/libs/image/jpeg_encode.h + src/eez/libs/image/toojpeg.h +) +list (APPEND header_files ${src_eez_libs_image}) +source_group("eez\\libs\\image" FILES ${src_eez_libs_image} ${header_eez_libs_image}) + set(src_eez_modules_aux_ps src/eez/modules/aux_ps/fan.cpp src/eez/modules/aux_ps/pid.cpp diff --git a/modular-psu-firmware.eez-project b/modular-psu-firmware.eez-project index f43050558..4d27ad060 100644 --- a/modular-psu-firmware.eez-project +++ b/modular-psu-firmware.eez-project @@ -2450,6 +2450,9 @@ }, { "name": "show_recordings_view" + }, + { + "name": "select_user_switch_action" } ], "extensionDefinitions": [ diff --git a/src/eez/gui/action_impl.cpp b/src/eez/gui/action_impl.cpp index 06f2160d2..ba799ef51 100644 --- a/src/eez/gui/action_impl.cpp +++ b/src/eez/gui/action_impl.cpp @@ -991,24 +991,82 @@ void action_scripts_next_page() { } void action_user_switch_clicked() { -#if OPTION_ENCODER - #if EEZ_PLATFORM_SIMULATOR AppContext *saved = g_appContext; g_appContext = &psu::gui::g_psuAppContext; #endif - if (getActivePageId() == PAGE_ID_EDIT_MODE_STEP) { - psu::gui::edit_mode_step::switchToNextStepIndex(); - } else { - mcu::encoder::switchEncoderMode(); - psu::gui::edit_mode_step::showCurrentEncoderMode(); + switch (persist_conf::devConf.userSwitchAction) { + case persist_conf::USER_SWITCH_ACTION_NONE: + break; + + case persist_conf::USER_SWITCH_ACTION_ENCODER_STEP: +#if OPTION_ENCODER + if (getActivePageId() == PAGE_ID_EDIT_MODE_STEP) { + psu::gui::edit_mode_step::switchToNextStepIndex(); + } else { + mcu::encoder::switchEncoderMode(); + psu::gui::edit_mode_step::showCurrentEncoderMode(); + } +#endif + break; + + case persist_conf::USER_SWITCH_ACTION_SCREENSHOT: +#if OPTION_SD_CARD + using namespace scpi; + osMessagePut(g_scpiMessageQueueId, SCPI_QUEUE_MESSAGE(SCPI_QUEUE_MESSAGE_TARGET_NONE, SCPI_QUEUE_MESSAGE_SCREENSHOT, 0), osWaitForever); +#endif + break; + + case persist_conf::USER_SWITCH_ACTION_MANUAL_TRIGGER: + action_trigger_generate_manual(); + break; + + case persist_conf::USER_SWITCH_ACTION_OUTPUT_ENABLE: + for (int i = 0; i < CH_NUM; ++i) { + Channel &channel = Channel::get(i); + if (channel.flags.trackingEnabled) { + channel_dispatcher::outputEnable(channel, !channel.isOutputEnabled()); + return; + } + } + infoMessage("Tracking is not enabled."); + break; + + case persist_conf::USER_SWITCH_ACTION_HOME: + if (g_appContext->getNumPagesOnStack() > 1) { + action_show_previous_page(); + } else { + showMainPage(); + } + break; + + case persist_conf::USER_SWITCH_ACTION_INHIBIT: + io_pins::setIsInhibitedByUser(!io_pins::getIsInhibitedByUser()); + break; + + case persist_conf::USER_SWITCH_ACTION_SELECTED_ACTION: + // TODO + break; } +} +void onSetUserSwitchAction(uint16_t value) { + popPage(); + persist_conf::setUserSwitchAction((persist_conf::UserSwitchAction)value); +} + +void action_select_user_switch_action() { #if EEZ_PLATFORM_SIMULATOR - g_appContext = saved; + AppContext *saved = g_appContext; + g_appContext = &psu::gui::g_psuAppContext; #endif + clearFoundWidgetAtDown(); + pushSelectFromEnumPage(g_userSwitchActionEnumDefinition, persist_conf::devConf.userSwitchAction, nullptr, onSetUserSwitchAction); + +#if EEZ_PLATFORM_SIMULATOR + g_appContext = saved; #endif } diff --git a/src/eez/gui/app_context.cpp b/src/eez/gui/app_context.cpp index d39b37bd3..864587b60 100644 --- a/src/eez/gui/app_context.cpp +++ b/src/eez/gui/app_context.cpp @@ -83,6 +83,11 @@ void AppContext::stateManagment() { return; } } + + if (m_showInfoMessageOnNextIter) { + gui::infoMessage(m_showInfoMessageOnNextIter); + m_showInfoMessageOnNextIter = nullptr; + } } //////////////////////////////////////////////////////////////////////////////// @@ -420,6 +425,14 @@ void AppContext::updateAppView(WidgetCursor &widgetCursor) { } } +int AppContext::getLongTouchActionHook(const WidgetCursor &widgetCursor) { + return ACTION_ID_NONE; +} + +void AppContext::infoMessage(const char *message) { + m_showInfoMessageOnNextIter = message; +} + } // namespace gui } // namespace eez diff --git a/src/eez/gui/app_context.h b/src/eez/gui/app_context.h index e9f33ab7e..acc1bc68c 100644 --- a/src/eez/gui/app_context.h +++ b/src/eez/gui/app_context.h @@ -104,11 +104,15 @@ class AppContext { void updateAppView(WidgetCursor &widgetCursor); - protected: - virtual int getMainPageId() = 0; - virtual void onPageChanged(int previousPageId, int activePageId); + virtual int getLongTouchActionHook(const WidgetCursor &widgetCursor); + + int getNumPagesOnStack() { + return m_pageNavigationStackPointer + 1; + } - // + void infoMessage(const char *message); + +protected: PageOnStack m_pageNavigationStack[CONF_GUI_PAGE_NAVIGATION_STACK_SIZE]; int m_pageNavigationStackPointer = 0; int m_activePageIndex; @@ -119,6 +123,11 @@ class AppContext { SelectFromEnumPage m_selectFromEnumPage; + const char *m_showInfoMessageOnNextIter; + + virtual int getMainPageId() = 0; + virtual void onPageChanged(int previousPageId, int activePageId); + void doShowPage(int index, Page *page, int previousPageId); void setPage(int pageId); diff --git a/src/eez/gui/dialogs.cpp b/src/eez/gui/dialogs.cpp index e4ea6ed2f..16ec08ab8 100644 --- a/src/eez/gui/dialogs.cpp +++ b/src/eez/gui/dialogs.cpp @@ -85,7 +85,11 @@ void pushToastMessage(ToastMessagePage *toastMessage) { } void infoMessage(const char *message) { - pushToastMessage(ToastMessagePage::create(INFO_TOAST, message)); + if (osThreadGetId() != g_guiTaskHandle) { + eez::psu::gui::g_psuAppContext.infoMessage(message); + } else { + pushToastMessage(ToastMessagePage::create(INFO_TOAST, message)); + } } void infoMessage(data::Value value) { diff --git a/src/eez/gui/event.cpp b/src/eez/gui/event.cpp index 69ad9527a..94ebd7cfb 100644 --- a/src/eez/gui/event.cpp +++ b/src/eez/gui/event.cpp @@ -63,6 +63,10 @@ WidgetCursor &getFoundWidgetAtDown() { return m_foundWidgetAtDown; } +void clearFoundWidgetAtDown() { + m_foundWidgetAtDown = WidgetCursor(); +} + bool isActiveWidget(const WidgetCursor &widgetCursor) { if (widgetCursor.appContext->isActiveWidget(widgetCursor)) { return true; @@ -77,6 +81,10 @@ int getAction(const WidgetCursor &widgetCursor) { } void onWidgetDefaultTouch(const WidgetCursor &widgetCursor, Event &touchEvent) { + if (!widgetCursor.widget) { + return; + } + if (touchEvent.type == EVENT_TYPE_TOUCH_DOWN) { m_touchActionExecuted = false; m_touchActionExecutedAtDown = false; @@ -105,6 +113,12 @@ void onWidgetDefaultTouch(const WidgetCursor &widgetCursor, Event &touchEvent) { m_touchActionExecuted = true; executeAction(action); } + } else if (touchEvent.type == EVENT_TYPE_LONG_TOUCH) { + m_touchActionExecuted = true; + int action = widgetCursor.appContext->getLongTouchActionHook(widgetCursor); + if (action != ACTION_ID_NONE) { + executeAction(action); + } } else if (touchEvent.type == EVENT_TYPE_TOUCH_UP) { if (!m_touchActionExecutedAtDown) { m_activeWidget = 0; diff --git a/src/eez/gui/gui.h b/src/eez/gui/gui.h index 1a6d270e7..49fdba9bd 100644 --- a/src/eez/gui/gui.h +++ b/src/eez/gui/gui.h @@ -18,6 +18,8 @@ #pragma once +#include + #include #include #include @@ -41,11 +43,13 @@ bool onSystemStateChanged(); //////////////////////////////////////////////////////////////////////////////// +extern osThreadId g_guiTaskHandle; extern bool g_isBlinkTime; //////////////////////////////////////////////////////////////////////////////// WidgetCursor &getFoundWidgetAtDown(); +void clearFoundWidgetAtDown(); bool isActiveWidget(const WidgetCursor &widgetCursor); uint32_t getShowPageTime(); void setShowPageTime(uint32_t time); diff --git a/src/eez/gui/page.cpp b/src/eez/gui/page.cpp index bb00e4875..79a933a88 100644 --- a/src/eez/gui/page.cpp +++ b/src/eez/gui/page.cpp @@ -320,7 +320,7 @@ void SelectFromEnumPage::init(const data::EnumItem *enumDefinition_, uint16_t cu bool (*disabledCallback_)(uint16_t value), void (*onSet_)(uint16_t)) { enumDefinition = enumDefinition_; - enumDefinitionFunc = NULL; + enumDefinitionFunc = nullptr; currentValue = currentValue_; disabledCallback = disabledCallback_; onSet = onSet_; @@ -331,7 +331,7 @@ void SelectFromEnumPage::init(const data::EnumItem *enumDefinition_, uint16_t cu void SelectFromEnumPage::init(void (*enumDefinitionFunc_)(data::DataOperationEnum operation, data::Cursor &cursor, data::Value &value), uint16_t currentValue_, bool (*disabledCallback_)(uint16_t value), void (*onSet_)(uint16_t)) { - enumDefinition = NULL; + enumDefinition = nullptr; enumDefinitionFunc = enumDefinitionFunc_; currentValue = currentValue_; disabledCallback = disabledCallback_; @@ -340,41 +340,12 @@ void SelectFromEnumPage::init(void (*enumDefinitionFunc_)(data::DataOperationEnu init(); } -void SelectFromEnumPage::init() { - const Style *containerStyle = getStyle(STYLE_ID_SELECT_ENUM_ITEM_POPUP_CONTAINER); - const Style *itemStyle = getStyle(STYLE_ID_SELECT_ENUM_ITEM_POPUP_ITEM); - - font::Font font = styleGetFont(itemStyle); - - // calculate geometry - itemHeight = itemStyle->padding_left + font.getHeight() + itemStyle->padding_right; - itemWidth = 0; - int i; - - char text[64]; - - for (i = 0; getLabel(i); ++i) { - getItemLabel(i, text, sizeof(text)); - int width = display::measureStr(text, -1, font); - if (width > itemWidth) { - itemWidth = width; - } - } - - numItems = i; - - itemWidth = itemStyle->padding_left + itemWidth + itemStyle->padding_right; - - width = containerStyle->padding_left + itemWidth + containerStyle->padding_right; - if (width > display::getDisplayWidth()) { - width = display::getDisplayWidth(); - } - - height = - containerStyle->padding_top + numItems * itemHeight + containerStyle->padding_bottom; - if (height > display::getDisplayHeight()) { - height = display::getDisplayHeight(); +void SelectFromEnumPage::init() { + numColumns = 1; + if (!calcSize()) { + numColumns = 2; + calcSize(); } findPagePosition(); @@ -406,19 +377,68 @@ bool SelectFromEnumPage::isDisabled(int i) { return disabledCallback && disabledCallback(getValue(i)); } +bool SelectFromEnumPage::calcSize() { + const Style *containerStyle = getStyle(STYLE_ID_SELECT_ENUM_ITEM_POPUP_CONTAINER); + const Style *itemStyle = getStyle(STYLE_ID_SELECT_ENUM_ITEM_POPUP_ITEM); + + font::Font font = styleGetFont(itemStyle); + + // calculate geometry + itemHeight = itemStyle->padding_top + font.getHeight() + itemStyle->padding_bottom; + itemWidth = 0; + + char text[64]; + + numItems = 0; + for (int i = 0; getLabel(i); ++i) { + ++numItems; + } + + for (int i = 0; i < numItems; ++i) { + getItemLabel(i, text, sizeof(text)); + int width = display::measureStr(text, -1, font); + if (width > itemWidth) { + itemWidth = width; + } + } + + itemWidth = itemStyle->padding_left + itemWidth + itemStyle->padding_right; + + width = containerStyle->padding_left + (numColumns == 2 ? itemWidth + containerStyle->padding_left + itemWidth : itemWidth) + containerStyle->padding_right; + if (width > g_appContext->width) { + width = g_appContext->width; + } + + height = + containerStyle->padding_top + (numColumns == 2 ? (numItems + 1) / 2 : numItems) * itemHeight + containerStyle->padding_bottom; + if (height > g_appContext->height) { + if (numColumns == 1) { + return false; + } + height = g_appContext->height; + } + + return true; +} + void SelectFromEnumPage::findPagePosition() { - const WidgetCursor &widgetCursorAtTouchDown = getFoundWidgetAtDown(); - x = widgetCursorAtTouchDown.x; - int right = g_appContext->x + g_appContext->width - 22; - if (x + width > right) { - x = right - width; - } + const WidgetCursor &widgetCursorAtTouchDown = getFoundWidgetAtDown(); + if (widgetCursorAtTouchDown.widget) { + x = widgetCursorAtTouchDown.x; + int right = g_appContext->x + g_appContext->width - 22; + if (x + width > right) { + x = right - width; + } - y = widgetCursorAtTouchDown.y + widgetCursorAtTouchDown.widget->h; - int bottom = g_appContext->y + g_appContext->height - 30; - if (y + height > bottom) { - y = bottom - height; - } + y = widgetCursorAtTouchDown.y + widgetCursorAtTouchDown.widget->h; + int bottom = g_appContext->y + g_appContext->height - 30; + if (y + height > bottom) { + y = bottom - height; + } + } else { + x = g_appContext->x + (g_appContext->width - width) / 2; + y = g_appContext->y + (g_appContext->height - height) / 2; + } } void SelectFromEnumPage::refresh() { @@ -450,9 +470,7 @@ void SelectFromEnumPage::updatePage() { } WidgetCursor SelectFromEnumPage::findWidget(int x, int y) { - int i; - - for (i = 0; getLabel(i); ++i) { + for (int i = 0; i < numItems; ++i) { int xItem, yItem; getItemPosition(i, xItem, yItem); if (!isDisabled(i)) { @@ -478,8 +496,13 @@ void SelectFromEnumPage::selectEnumItem() { void SelectFromEnumPage::getItemPosition(int itemIndex, int &xItem, int &yItem) { const Style *containerStyle = getStyle(STYLE_ID_SELECT_ENUM_ITEM_POPUP_CONTAINER); - xItem = x + containerStyle->padding_left; - yItem = y + containerStyle->padding_top + itemIndex * itemHeight; + if (numColumns == 1 || itemIndex < (numItems + 1) / 2) { + xItem = x + containerStyle->padding_left; + yItem = y + containerStyle->padding_top + itemIndex * itemHeight; + } else { + xItem = x + containerStyle->padding_left + itemWidth + containerStyle->padding_left / 2; + yItem = y + containerStyle->padding_top + (itemIndex - (numItems + 1) / 2) * itemHeight; + } } void SelectFromEnumPage::getItemLabel(int itemIndex, char *text, int count) { diff --git a/src/eez/gui/page.h b/src/eez/gui/page.h index 0bcf28986..998b38000 100644 --- a/src/eez/gui/page.h +++ b/src/eez/gui/page.h @@ -57,7 +57,7 @@ class InternalPage : public Page { public: virtual void refresh() = 0; // repaint page virtual void updatePage() = 0; - virtual WidgetCursor findWidget(int x, int y) = 0; + virtual WidgetCursor findWidget(int x, int y) = 0; int x; int y; @@ -123,11 +123,16 @@ class SelectFromEnumPage : public InternalPage { void selectEnumItem(); + const data::EnumItem *getEnumDefinition() { + return enumDefinition; + } + private: const data::EnumItem *enumDefinition; void (*enumDefinitionFunc)(data::DataOperationEnum operation, data::Cursor &cursor, data::Value &value); int numItems; + int numColumns; int itemWidth; int itemHeight; @@ -140,6 +145,7 @@ class SelectFromEnumPage : public InternalPage { bool (*disabledCallback)(uint16_t value); void (*onSet)(uint16_t); + bool calcSize(); void findPagePosition(); bool isDisabled(int i); diff --git a/src/eez/gui/widget.cpp b/src/eez/gui/widget.cpp index b01c069cc..295104807 100644 --- a/src/eez/gui/widget.cpp +++ b/src/eez/gui/widget.cpp @@ -353,18 +353,25 @@ void findWidgetStep(const WidgetCursor &widgetCursor) { } WidgetCursor findWidget(int16_t x, int16_t y) { + g_foundWidget = 0; + if (g_appContext->isActivePageInternal()) { WidgetCursor widgetCursor = ((InternalPage *)g_appContext->getActivePage())->findWidget(x, y); if (!widgetCursor) { // clicked outside internal page, close internal page + bool passThrough = g_appContext->getActivePageId() == INTERNAL_PAGE_ID_TOAST_MESSAGE; + popPage(); + + if (!passThrough) { + return g_foundWidget; + } } else { return widgetCursor; } } - g_foundWidget = 0; g_findWidgetAtX = x; g_findWidgetAtY = y; @@ -377,4 +384,4 @@ WidgetCursor findWidget(int16_t x, int16_t y) { } // namespace gui } // namespace eez -#endif \ No newline at end of file +#endif diff --git a/src/eez/libs/image/jpeg_encode.cpp b/src/eez/libs/image/jpeg_encode.cpp new file mode 100644 index 000000000..a9f21aa41 --- /dev/null +++ b/src/eez/libs/image/jpeg_encode.cpp @@ -0,0 +1,50 @@ +/* + * EEZ Modular Firmware + * Copyright (C) 2015-present, Envox d.o.o. + * + * This program is free software: you can redistribute it and/or modify + * it 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. + + * This program is distributed in the hope that it 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 program. If not, see . + */ + +#include +#include +#include + +#include "toojpeg.h" + +#include + +#if defined(EEZ_PLATFORM_STM32) +#include +static const size_t MAX_IMAGE_DATA_SIZE = VRAM_SCREENSHOOT_JPEG_OUT_BUFFER_SIZE; +static uint8_t *g_imageData = (uint8_t *)VRAM_SCREENSHOOT_JPEG_OUT_BUFFER; +#endif + +#if defined(EEZ_PLATFORM_SIMULATOR) +static const size_t MAX_IMAGE_DATA_SIZE = 256 * 1024; +static uint8_t g_imageData[MAX_IMAGE_DATA_SIZE]; +#endif + +static size_t g_imageDataSize; + +void WRITE_ONE_BYTE(unsigned char byte) { + g_imageData[g_imageDataSize++] = byte; +} + +int jpegEncode(const uint8_t *screenshotPixels, unsigned char **imageData, size_t *imageDataSize) { + g_imageDataSize = 0; + TooJpeg::writeJpeg(WRITE_ONE_BYTE, screenshotPixels, 480, 272); + *imageData = g_imageData; + *imageDataSize = g_imageDataSize; + return 0; +} diff --git a/src/eez/libs/image/jpeg_encode.h b/src/eez/libs/image/jpeg_encode.h new file mode 100644 index 000000000..87cbd394f --- /dev/null +++ b/src/eez/libs/image/jpeg_encode.h @@ -0,0 +1,21 @@ +/* + * EEZ Modular Firmware + * Copyright (C) 2015-present, Envox d.o.o. + * + * This program is free software: you can redistribute it and/or modify + * it 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. + + * This program is distributed in the hope that it 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 program. If not, see . + */ + +#pragma once + +int jpegEncode(const uint8_t *screenshotPixels, unsigned char **imageData, size_t *imageDataSize); \ No newline at end of file diff --git a/src/eez/libs/image/toojpeg.cpp b/src/eez/libs/image/toojpeg.cpp new file mode 100644 index 000000000..c68de2c38 --- /dev/null +++ b/src/eez/libs/image/toojpeg.cpp @@ -0,0 +1,572 @@ +// ////////////////////////////////////////////////////////// +// toojpeg.cpp +// written by Stephan Brumme, 2018-2019 +// see https://create.stephan-brumme.com/toojpeg/ +// +#include "toojpeg.h" +// - the "official" specifications: https://www.w3.org/Graphics/JPEG/itu-t81.pdf and https://www.w3.org/Graphics/JPEG/jfif3.pdf +// - Wikipedia has a short description of the JFIF/JPEG file format: https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format +// - the popular STB Image library includes Jon's JPEG encoder as well: https://github.com/nothings/stb/blob/master/stb_image_write.h +// - the most readable JPEG book (from a developer's perspective) is Miano's "Compressed Image File Formats" (1999, ISBN 0-201-60443-4), +// used copies are really cheap nowadays and include a CD with C++ sources as well (plus great format descriptions of GIF & PNG) +// - much more detailled is Mitchell/Pennebaker's "JPEG: Still Image Data Compression Standard" (1993, ISBN 0-442-01272-1) +// which contains the official JPEG standard, too - fun fact: I bought a signed copy in a second-hand store without noticing +namespace // anonymous namespace to hide local functions / constants / etc. +{ +// //////////////////////////////////////// +// data types +using uint8_t = unsigned char; +using uint16_t = unsigned short; +using int16_t = short; +using int32_t = int; // at least four bytes +// //////////////////////////////////////// +// constants +// quantization tables from JPEG Standard, Annex K +const uint8_t DefaultQuantLuminance[8*8] = + { 16, 11, 10, 16, 24, 40, 51, 61, // there are a few experts proposing slightly more efficient values, + 12, 12, 14, 19, 26, 58, 60, 55, // e.g. https://www.imagemagick.org/discourse-server/viewtopic.php?t=20333 + 14, 13, 16, 24, 40, 57, 69, 56, // btw: Google's Guetzli project optimizes the quantization tables per image + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 }; +const uint8_t DefaultQuantChrominance[8*8] = + { 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 }; +// 8x8 blocks are processed in zig-zag order +// most encoders use a zig-zag "forward" table, I switched to its inverse for performance reasons +// note: ZigZagInv[ZigZag[i]] = i +const uint8_t ZigZagInv[8*8] = + { 0, 1, 8,16, 9, 2, 3,10, // ZigZag[] = 0, 1, 5, 6,14,15,27,28, + 17,24,32,25,18,11, 4, 5, // 2, 4, 7,13,16,26,29,42, + 12,19,26,33,40,48,41,34, // 3, 8,12,17,25,30,41,43, + 27,20,13, 6, 7,14,21,28, // 9,11,18,24,31,40,44,53, + 35,42,49,56,57,50,43,36, // 10,19,23,32,39,45,52,54, + 29,22,15,23,30,37,44,51, // 20,22,33,38,46,51,55,60, + 58,59,52,45,38,31,39,46, // 21,34,37,47,50,56,59,61, + 53,60,61,54,47,55,62,63 }; // 35,36,48,49,57,58,62,63 +// static Huffman code tables from JPEG standard Annex K +// - CodesPerBitsize tables define how many Huffman codes will have a certain bitsize (plus 1 because there nothing with zero bits), +// e.g. DcLuminanceCodesPerBitsize[2] = 5 because there are 5 Huffman codes being 2+1=3 bits long +// - Values tables are a list of values ordered by their Huffman code bitsize, +// e.g. AcLuminanceValues => Huffman(0x01,0x02 and 0x03) will have 2 bits, Huffman(0x00) will have 3 bits, Huffman(0x04,0x11 and 0x05) will have 4 bits, ... +// Huffman definitions for first DC/AC tables (luminance / Y channel) +const uint8_t DcLuminanceCodesPerBitsize[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; // sum = 12 +const uint8_t DcLuminanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes +const uint8_t AcLuminanceCodesPerBitsize[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 }; // sum = 162 +const uint8_t AcLuminanceValues [162] = // => 162 codes + { 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, // 16*10+2 symbols because + 0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, // upper 4 bits can be 0..F + 0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, // while lower 4 bits can be 1..A + 0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, // plus two special codes 0x00 and 0xF0 + 0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, // order of these symbols was determined empirically by JPEG committee + 0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2, + 0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA }; +// Huffman definitions for second DC/AC tables (chrominance / Cb and Cr channels) +const uint8_t DcChrominanceCodesPerBitsize[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; // sum = 12 +const uint8_t DcChrominanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes (identical to DcLuminanceValues) +const uint8_t AcChrominanceCodesPerBitsize[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 }; // sum = 162 +const uint8_t AcChrominanceValues [162] = // => 162 codes + { 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, // same number of symbol, just different order + 0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, // (which is more efficient for AC coding) + 0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4, + 0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA, + 0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA }; +const int16_t CodeWordLimit = 2048; // +/-2^11, maximum value after DCT +// //////////////////////////////////////// +// structs +// represent a single Huffman code +struct BitCode +{ + BitCode() = default; // undefined state, must be initialized at a later time + BitCode(uint16_t code_, uint8_t numBits_) + : code(code_), numBits(numBits_) {} + uint16_t code; // JPEG's Huffman codes are limited to 16 bits + uint8_t numBits; // number of valid bits +}; +// wrapper for bit output operations +struct BitWriter +{ + // user-supplied callback that writes/stores one byte + TooJpeg::WRITE_ONE_BYTE output; + // initialize writer + explicit BitWriter(TooJpeg::WRITE_ONE_BYTE output_) : output(output_) {} + // store the most recently encoded bits that are not written yet + struct BitBuffer + { + int32_t data = 0; // actually only at most 24 bits are used + uint8_t numBits = 0; // number of valid bits (the right-most bits) + } buffer; + // write Huffman bits stored in BitCode, keep excess bits in BitBuffer + BitWriter& operator<<(const BitCode& data) + { + // append the new bits to those bits leftover from previous call(s) + buffer.numBits += data.numBits; + buffer.data <<= data.numBits; + buffer.data |= data.code; + // write all "full" bytes + while (buffer.numBits >= 8) + { + // extract highest 8 bits + buffer.numBits -= 8; + auto oneByte = uint8_t(buffer.data >> buffer.numBits); + output(oneByte); + if (oneByte == 0xFF) // 0xFF has a special meaning for JPEGs (it's a block marker) + output(0); // therefore pad a zero to indicate "nope, this one ain't a marker, it's just a coincidence" + // note: I don't clear those written bits, therefore buffer.bits may contain garbage in the high bits + // if you really want to "clean up" (e.g. for debugging purposes) then uncomment the following line + //buffer.bits &= (1 << buffer.numBits) - 1; + } + return *this; + } + // write all non-yet-written bits, fill gaps with 1s (that's a strange JPEG thing) + void flush() + { + // at most seven set bits needed to "fill" the last byte: 0x7F = binary 0111 1111 + *this << BitCode(0x7F, 7); // I should set buffer.numBits = 0 but since there are no single bits written after flush() I can safely ignore it + } + // NOTE: all the following BitWriter functions IGNORE the BitBuffer and write straight to output ! + // write a single byte + BitWriter& operator<<(uint8_t oneByte) + { + output(oneByte); + return *this; + } + // write an array of bytes + template + BitWriter& operator<<(T (&manyBytes)[Size]) + { + for (auto c : manyBytes) + output(c); + return *this; + } + // start a new JFIF block + void addMarker(uint8_t id, uint16_t length) + { + output(0xFF); output(id); // ID, always preceded by 0xFF + output(uint8_t(length >> 8)); // length of the block (big-endian, includes the 2 length bytes as well) + output(uint8_t(length & 0xFF)); + } +}; +// //////////////////////////////////////// +// functions / templates +// same as std::min() +template +Number minimum(Number value, Number maximum) +{ + return value <= maximum ? value : maximum; +} +// restrict a value to the interval [minimum, maximum] +template +Number clamp(Number value, Limit minValue, Limit maxValue) +{ + if (value <= minValue) return minValue; // never smaller than the minimum + if (value >= maxValue) return maxValue; // never bigger than the maximum + return value; // value was inside interval, keep it +} +// convert from RGB to YCbCr, constants are similar to ITU-R, see https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion +float rgb2y (float r, float g, float b) { return +0.299f * r +0.587f * g +0.114f * b; } +float rgb2cb(float r, float g, float b) { return -0.16874f * r -0.33126f * g +0.5f * b; } +float rgb2cr(float r, float g, float b) { return +0.5f * r -0.41869f * g -0.08131f * b; } +// forward DCT computation "in one dimension" (fast AAN algorithm by Arai, Agui and Nakajima: "A fast DCT-SQ scheme for images") +void DCT(float block[8*8], uint8_t stride) // stride must be 1 (=horizontal) or 8 (=vertical) +{ + const auto SqrtHalfSqrt = 1.306562965f; // sqrt((2 + sqrt(2)) / 2) = cos(pi * 1 / 8) * sqrt(2) + const auto InvSqrt = 0.707106781f; // 1 / sqrt(2) = cos(pi * 2 / 8) + const auto HalfSqrtSqrt = 0.382683432f; // sqrt(2 - sqrt(2)) / 2 = cos(pi * 3 / 8) + const auto InvSqrtSqrt = 0.541196100f; // 1 / sqrt(2 - sqrt(2)) = cos(pi * 3 / 8) * sqrt(2) + // modify in-place + auto& block0 = block[0 ]; + auto& block1 = block[1 * stride]; + auto& block2 = block[2 * stride]; + auto& block3 = block[3 * stride]; + auto& block4 = block[4 * stride]; + auto& block5 = block[5 * stride]; + auto& block6 = block[6 * stride]; + auto& block7 = block[7 * stride]; + // based on https://dev.w3.org/Amaya/libjpeg/jfdctflt.c , the original variable names can be found in my comments + auto add07 = block0 + block7; auto sub07 = block0 - block7; // tmp0, tmp7 + auto add16 = block1 + block6; auto sub16 = block1 - block6; // tmp1, tmp6 + auto add25 = block2 + block5; auto sub25 = block2 - block5; // tmp2, tmp5 + auto add34 = block3 + block4; auto sub34 = block3 - block4; // tmp3, tmp4 + auto add0347 = add07 + add34; auto sub07_34 = add07 - add34; // tmp10, tmp13 ("even part" / "phase 2") + auto add1256 = add16 + add25; auto sub16_25 = add16 - add25; // tmp11, tmp12 + block0 = add0347 + add1256; block4 = add0347 - add1256; // "phase 3" + auto z1 = (sub16_25 + sub07_34) * InvSqrt; // all temporary z-variables kept their original names + block2 = sub07_34 + z1; block6 = sub07_34 - z1; // "phase 5" + auto sub23_45 = sub25 + sub34; // tmp10 ("odd part" / "phase 2") + auto sub12_56 = sub16 + sub25; // tmp11 + auto sub01_67 = sub16 + sub07; // tmp12 + auto z5 = (sub23_45 - sub01_67) * HalfSqrtSqrt; + auto z2 = sub23_45 * InvSqrtSqrt + z5; + auto z3 = sub12_56 * InvSqrt; + auto z4 = sub01_67 * SqrtHalfSqrt + z5; + auto z6 = sub07 + z3; // z11 ("phase 5") + auto z7 = sub07 - z3; // z13 + block1 = z6 + z4; block7 = z6 - z4; // "phase 6" + block5 = z7 + z2; block3 = z7 - z2; +} +// run DCT, quantize and write Huffman bit codes +int16_t encodeBlock(BitWriter& writer, float block[8][8], const float scaled[8*8], int16_t lastDC, + const BitCode huffmanDC[256], const BitCode huffmanAC[256], const BitCode* codewords) +{ + // "linearize" the 8x8 block, treat it as a flat array of 64 floats + auto block64 = (float*) block; + // DCT: rows + for (auto offset = 0; offset < 8; offset++) + DCT(block64 + offset*8, 1); + // DCT: columns + for (auto offset = 0; offset < 8; offset++) + DCT(block64 + offset*1, 8); + // scale + for (auto i = 0; i < 8*8; i++) + block64[i] *= scaled[i]; + // encode DC (the first coefficient is the "average color" of the 8x8 block) + auto DC = int(block64[0] + (block64[0] >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect + // quantize and zigzag the other 63 coefficients + auto posNonZero = 0; // find last coefficient which is not zero (because trailing zeros are encoded differently) + int16_t quantized[8*8]; + for (auto i = 1; i < 8*8; i++) // start at 1 because block64[0]=DC was already processed + { + auto value = block64[ZigZagInv[i]]; + // round to nearest integer + quantized[i] = int(value + (value >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect + // remember offset of last non-zero coefficient + if (quantized[i] != 0) + posNonZero = i; + } + // same "average color" as previous block ? + auto diff = DC - lastDC; + if (diff == 0) + writer << huffmanDC[0x00]; // yes, write a special short symbol + else + { + auto bits = codewords[diff]; // nope, encode the difference to previous block's average color + writer << huffmanDC[bits.numBits] << bits; + } + // encode ACs (quantized[1..63]) + auto offset = 0; // upper 4 bits count the number of consecutive zeros + for (auto i = 1; i <= posNonZero; i++) // quantized[0] was already written, skip all trailing zeros, too + { + // zeros are encoded in a special way + while (quantized[i] == 0) // found another zero ? + { + offset += 0x10; // add 1 to the upper 4 bits + // split into blocks of at most 16 consecutive zeros + if (offset > 0xF0) // remember, the counter is in the upper 4 bits, 0xF = 15 + { + writer << huffmanAC[0xF0]; // 0xF0 is a special code for "16 zeros" + offset = 0; + } + i++; + } + auto encoded = codewords[quantized[i]]; + // combine number of zeros with the number of bits of the next non-zero value + writer << huffmanAC[offset + encoded.numBits] << encoded; // and the value itself + offset = 0; + } + // send end-of-block code (0x00), only needed if there are trailing zeros + if (posNonZero < 8*8 - 1) // = 63 + writer << huffmanAC[0x00]; + return DC; +} +// Jon's code includes the pre-generated Huffman codes +// I don't like these "magic constants" and compute them on my own :-) +void generateHuffmanTable(const uint8_t numCodes[16], const uint8_t* values, BitCode result[256]) +{ + // process all bitsizes 1 thru 16, no JPEG Huffman code is allowed to exceed 16 bits + auto huffmanCode = 0; + for (auto numBits = 1; numBits <= 16; numBits++) + { + // ... and each code of these bitsizes + for (auto i = 0; i < numCodes[numBits - 1]; i++) // note: numCodes array starts at zero, but smallest bitsize is 1 + result[*values++] = BitCode(huffmanCode++, numBits); + // next Huffman code needs to be one bit wider + huffmanCode <<= 1; + } +} +} // end of anonymous namespace +// -------------------- externally visible code -------------------- +namespace TooJpeg +{ +// the only exported function ... +bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels_, unsigned short width, unsigned short height, + bool isRGB, unsigned char quality_, bool downsample, const char* comment) +{ + // reject invalid pointers + if (output == nullptr || pixels_ == nullptr) + return false; + // check image format + if (width == 0 || height == 0) + return false; + // number of components + const auto numComponents = isRGB ? 3 : 1; + // note: if there is just one component (=grayscale), then only luminance needs to be stored in the file + // thus everything related to chrominance need not to be written to the JPEG + // I still compute a few things, like quantization tables to avoid a complete code mess + // grayscale images can't be downsampled (because there are no Cb + Cr channels) + if (!isRGB) + downsample = false; + // wrapper for all output operations + BitWriter bitWriter(output); + // //////////////////////////////////////// + // JFIF headers + const uint8_t HeaderJfif[2+2+16] = + { 0xFF,0xD8, // SOI marker (start of image) + 0xFF,0xE0, // JFIF APP0 tag + 0,16, // length: 16 bytes (14 bytes payload + 2 bytes for this length field) + 'J','F','I','F',0, // JFIF identifier, zero-terminated + 1,1, // JFIF version 1.1 + 0, // no density units specified + 0,1,0,1, // density: 1 pixel "per pixel" horizontally and vertically + 0,0 }; // no thumbnail (size 0 x 0) + bitWriter << HeaderJfif; + // //////////////////////////////////////// + // comment (optional) + if (comment != nullptr) + { + // look for zero terminator + auto length = 0; // = strlen(comment); + while (comment[length] != 0) + length++; + // write COM marker + bitWriter.addMarker(0xFE, 2+length); // block size is number of bytes (without zero terminator) + 2 bytes for this length field + // ... and write the comment itself + for (auto i = 0; i < length; i++) + bitWriter << comment[i]; + } + // //////////////////////////////////////// + // adjust quantization tables to desired quality + // quality level must be in 1 ... 100 + auto quality = clamp(quality_, 1, 100); + // convert to an internal JPEG quality factor, formula taken from libjpeg + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + uint8_t quantLuminance [8*8]; + uint8_t quantChrominance[8*8]; + for (auto i = 0; i < 8*8; i++) + { + int luminance = (DefaultQuantLuminance [ZigZagInv[i]] * quality + 50) / 100; + int chrominance = (DefaultQuantChrominance[ZigZagInv[i]] * quality + 50) / 100; + // clamp to 1..255 + quantLuminance [i] = clamp(luminance, 1, 255); + quantChrominance[i] = clamp(chrominance, 1, 255); + } + // write quantization tables + bitWriter.addMarker(0xDB, 2 + (isRGB ? 2 : 1) * (1 + 8*8)); // length: 65 bytes per table + 2 bytes for this length field + // each table has 64 entries and is preceded by an ID byte + bitWriter << 0x00 << quantLuminance; // first quantization table + if (isRGB) + bitWriter << 0x01 << quantChrominance; // second quantization table, only relevant for color images + // //////////////////////////////////////// + // write image infos (SOF0 - start of frame) + bitWriter.addMarker(0xC0, 2+6+3*numComponents); // length: 6 bytes general info + 3 per channel + 2 bytes for this length field + // 8 bits per channel + bitWriter << 0x08 + // image dimensions (big-endian) + << (height >> 8) << (height & 0xFF) + << (width >> 8) << (width & 0xFF); + // sampling and quantization tables for each component + bitWriter << numComponents; // 1 component (grayscale, Y only) or 3 components (Y,Cb,Cr) + for (auto id = 1; id <= numComponents; id++) + bitWriter << id // component ID (Y=1, Cb=2, Cr=3) + // bitmasks for sampling: highest 4 bits: horizontal, lowest 4 bits: vertical + << (id == 1 && downsample ? 0x22 : 0x11) // 0x11 is default YCbCr 4:4:4 and 0x22 stands for YCbCr 4:2:0 + << (id == 1 ? 0 : 1); // use quantization table 0 for Y, table 1 for Cb and Cr + // //////////////////////////////////////// + // Huffman tables + // DHT marker - define Huffman tables + bitWriter.addMarker(0xC4, isRGB ? (2+208+208) : (2+208)); + // 2 bytes for the length field, store chrominance only if needed + // 1+16+12 for the DC luminance + // 1+16+162 for the AC luminance (208 = 1+16+12 + 1+16+162) + // 1+16+12 for the DC chrominance + // 1+16+162 for the AC chrominance (208 = 1+16+12 + 1+16+162, same as above) + // store luminance's DC+AC Huffman table definitions + bitWriter << 0x00 // highest 4 bits: 0 => DC, lowest 4 bits: 0 => Y (baseline) + << DcLuminanceCodesPerBitsize + << DcLuminanceValues; + bitWriter << 0x10 // highest 4 bits: 1 => AC, lowest 4 bits: 0 => Y (baseline) + << AcLuminanceCodesPerBitsize + << AcLuminanceValues; + // compute actual Huffman code tables (see Jon's code for precalculated tables) + BitCode huffmanLuminanceDC[256]; + BitCode huffmanLuminanceAC[256]; + generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC); + generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC); + // chrominance is only relevant for color images + BitCode huffmanChrominanceDC[256]; + BitCode huffmanChrominanceAC[256]; + if (isRGB) + { + // store luminance's DC+AC Huffman table definitions + bitWriter << 0x01 // highest 4 bits: 0 => DC, lowest 4 bits: 1 => Cr,Cb (baseline) + << DcChrominanceCodesPerBitsize + << DcChrominanceValues; + bitWriter << 0x11 // highest 4 bits: 1 => AC, lowest 4 bits: 1 => Cr,Cb (baseline) + << AcChrominanceCodesPerBitsize + << AcChrominanceValues; + // compute actual Huffman code tables (see Jon's code for precalculated tables) + generateHuffmanTable(DcChrominanceCodesPerBitsize, DcChrominanceValues, huffmanChrominanceDC); + generateHuffmanTable(AcChrominanceCodesPerBitsize, AcChrominanceValues, huffmanChrominanceAC); + } + // //////////////////////////////////////// + // start of scan (there is only a single scan for baseline JPEGs) + bitWriter.addMarker(0xDA, 2+1+2*numComponents+3); // 2 bytes for the length field, 1 byte for number of components, + // then 2 bytes for each component and 3 bytes for spectral selection + // assign Huffman tables to each component + bitWriter << numComponents; + for (auto id = 1; id <= numComponents; id++) + // highest 4 bits: DC Huffman table, lowest 4 bits: AC Huffman table + bitWriter << id << (id == 1 ? 0x00 : 0x11); // Y: tables 0 for DC and AC; Cb + Cr: tables 1 for DC and AC + // constant values for our baseline JPEGs (which have a single sequential scan) + static const uint8_t Spectral[3] = { 0, 63, 0 }; // spectral selection: must be from 0 to 63; successive approximation must be 0 + bitWriter << Spectral; + // //////////////////////////////////////// + // adjust quantization tables with AAN scaling factors to simplify DCT + float scaledLuminance [8*8]; + float scaledChrominance[8*8]; + for (auto i = 0; i < 8*8; i++) + { + auto row = ZigZagInv[i] / 8; // same as ZigZagInv[i] >> 3 + auto column = ZigZagInv[i] % 8; // same as ZigZagInv[i] & 7 + // scaling constants for AAN DCT algorithm: AanScaleFactors[0] = 1, AanScaleFactors[k=1..7] = cos(k*PI/16) * sqrt(2) + static const float AanScaleFactors[8] = { 1, 1.387039845f, 1.306562965f, 1.175875602f, 1, 0.785694958f, 0.541196100f, 0.275899379f }; + auto factor = 1 / (AanScaleFactors[row] * AanScaleFactors[column] * 8); + scaledLuminance [ZigZagInv[i]] = factor / quantLuminance [i]; + scaledChrominance[ZigZagInv[i]] = factor / quantChrominance[i]; + // if you really want JPEGs that are bitwise identical to Jon Olick's code then you need slightly different formulas (note: sqrt(8) = 2.828427125f) + //static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; // line 240 of jo_jpeg.cpp + //scaledLuminance [ZigZagInv[i]] = 1 / (quantLuminance [i] * aasf[row] * aasf[column]); // lines 266-267 of jo_jpeg.cpp + //scaledChrominance[ZigZagInv[i]] = 1 / (quantChrominance[i] * aasf[row] * aasf[column]); + } + // //////////////////////////////////////// + // precompute JPEG codewords for quantized DCT + BitCode codewordsArray[2 * CodeWordLimit]; // note: quantized[i] is found at codewordsArray[quantized[i] + CodeWordLimit] + BitCode* codewords = &codewordsArray[CodeWordLimit]; // allow negative indices, so quantized[i] is at codewords[quantized[i]] + uint8_t numBits = 1; // each codeword has at least one bit (value == 0 is undefined) + int32_t mask = 1; // mask is always 2^numBits - 1, initial value 2^1-1 = 2-1 = 1 + for (int16_t value = 1; value < CodeWordLimit; value++) + { + // numBits = position of highest set bit (ignoring the sign) + // mask = (2^numBits) - 1 + if (value > mask) // one more bit ? + { + numBits++; + mask = (mask << 1) | 1; // append a set bit + } + codewords[-value] = BitCode(mask - value, numBits); // note that I use a negative index => codewords[-value] = codewordsArray[CodeWordLimit value] + codewords[+value] = BitCode( value, numBits); + } + // just convert image data from void* + auto pixels = (const uint8_t*)pixels_; + // the next two variables are frequently used when checking for image borders + const auto maxWidth = width - 1; // "last row" + const auto maxHeight = height - 1; // "bottom line" + // process MCUs (minimum codes units) => image is subdivided into a grid of 8x8 or 16x16 tiles + const auto sampling = downsample ? 2 : 1; // 1x1 or 2x2 sampling + const auto mcuSize = 8 * sampling; + // average color of the previous MCU + int16_t lastYDC = 0, lastCbDC = 0, lastCrDC = 0; + // convert from RGB to YCbCr + float Y[8][8], Cb[8][8], Cr[8][8]; + for (auto mcuY = 0; mcuY < height; mcuY += mcuSize) // each step is either 8 or 16 (=mcuSize) + for (auto mcuX = 0; mcuX < width; mcuX += mcuSize) + { + // YCbCr 4:4:4 format: each MCU is a 8x8 block - the same applies to grayscale images, too + // YCbCr 4:2:0 format: each MCU represents a 16x16 block, stored as 4x 8x8 Y-blocks plus 1x 8x8 Cb and 1x 8x8 Cr block) + for (auto blockY = 0; blockY < mcuSize; blockY += 8) // iterate once (YCbCr444 and grayscale) or twice (YCbCr420) + for (auto blockX = 0; blockX < mcuSize; blockX += 8) + { + // now we finally have an 8x8 block ... + for (auto deltaY = 0; deltaY < 8; deltaY++) + { + auto column = minimum(mcuX + blockX , maxWidth); // must not exceed image borders, replicate last row/column if needed + auto row = minimum(mcuY + blockY + deltaY, maxHeight); + for (auto deltaX = 0; deltaX < 8; deltaX++) + { + // find actual pixel position within the current image + auto pixelPos = row * int(width) + column; // the cast ensures that we don't run into multiplication overflows + if (column < maxWidth) + column++; + // grayscale images have solely a Y channel which can be easily derived from the input pixel by shifting it by 128 + if (!isRGB) + { + Y[deltaY][deltaX] = pixels[pixelPos] - 128.f; + continue; + } + // RGB: 3 bytes per pixel (whereas grayscale images have only 1 byte per pixel) + auto r = pixels[3 * pixelPos ]; + auto g = pixels[3 * pixelPos + 1]; + auto b = pixels[3 * pixelPos + 2]; + Y [deltaY][deltaX] = rgb2y (r, g, b) - 128; // again, the JPEG standard requires Y to be shifted by 128 + // YCbCr444 is easy - the more complex YCbCr420 has to be computed about 20 lines below in a second pass + if (!downsample) + { + Cb[deltaY][deltaX] = rgb2cb(r, g, b); // standard RGB-to-YCbCr conversion + Cr[deltaY][deltaX] = rgb2cr(r, g, b); + } + } + } + // encode Y channel + lastYDC = encodeBlock(bitWriter, Y, scaledLuminance, lastYDC, huffmanLuminanceDC, huffmanLuminanceAC, codewords); + // Cb and Cr are encoded about 50 lines below + } + // grayscale images don't need any Cb and Cr information + if (!isRGB) + continue; + // //////////////////////////////////////// + // the following lines are only relevant for YCbCr420: + // average/downsample chrominance of four pixels while respecting the image borders + if (downsample) + for (short deltaY = 7; downsample && deltaY >= 0; deltaY--) // iterating loop in reverse increases cache read efficiency + { + auto row = minimum(mcuY + 2*deltaY, maxHeight); // each deltaX/Y step covers a 2x2 area + auto column = mcuX; // column is updated inside next loop + auto pixelPos = (row * int(width) + column) * 3; // numComponents = 3 + // deltas (in bytes) to next row / column, must not exceed image borders + auto rowStep = (row < maxHeight) ? 3 * int(width) : 0; // always numComponents*width except for bottom line + auto columnStep = (column < maxWidth ) ? 3 : 0; // always numComponents except for rightmost pixel + for (short deltaX = 0; deltaX < 8; deltaX++) + { + // let's add all four samples (2x2 area) + auto right = pixelPos + columnStep; + auto down = pixelPos + rowStep; + auto downRight = pixelPos + columnStep + rowStep; + // note: cast from 8 bits to >8 bits to avoid overflows when adding + auto r = short(pixels[pixelPos ]) + pixels[right ] + pixels[down ] + pixels[downRight ]; + auto g = short(pixels[pixelPos + 1]) + pixels[right + 1] + pixels[down + 1] + pixels[downRight + 1]; + auto b = short(pixels[pixelPos + 2]) + pixels[right + 2] + pixels[down + 2] + pixels[downRight + 2]; + // convert to Cb and Cr + Cb[deltaY][deltaX] = rgb2cb(r, g, b) / 4; // I still have to divide r,g,b by 4 to get their average values + Cr[deltaY][deltaX] = rgb2cr(r, g, b) / 4; // it's a bit faster if done AFTER CbCr conversion + // step forward to next 2x2 area + pixelPos += 2*3; // 2 pixels => 6 bytes (2*numComponents) + column += 2; + // reached right border ? + if (column >= maxWidth) + { + columnStep = 0; + pixelPos = ((row + 1) * int(width) - 1) * 3; // same as (row * width + maxWidth) * numComponents => current's row last pixel + } + } + } // end of YCbCr420 code for Cb and Cr + // encode Cb and Cr + lastCbDC = encodeBlock(bitWriter, Cb, scaledChrominance, lastCbDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords); + lastCrDC = encodeBlock(bitWriter, Cr, scaledChrominance, lastCrDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords); + } + bitWriter.flush(); // now image is completely encoded, write any bits still left in the buffer + // /////////////////////////// + // EOI marker + bitWriter << 0xFF << 0xD9; // this marker has no length, therefore I can't use addMarker() + return true; +} // writeJpeg() +} // namespace TooJpeg \ No newline at end of file diff --git a/src/eez/libs/image/toojpeg.h b/src/eez/libs/image/toojpeg.h new file mode 100644 index 000000000..496028d3a --- /dev/null +++ b/src/eez/libs/image/toojpeg.h @@ -0,0 +1,57 @@ +// ////////////////////////////////////////////////////////// +// toojpeg.h +// written by Stephan Brumme, 2018-2019 +// see https://create.stephan-brumme.com/toojpeg/ +// +// This is a compact baseline JPEG/JFIF writer, written in C++ (but looks like C for the most part). +// Its interface has only one function: writeJpeg() - and that's it ! +// +// basic example: +// => create an image with any content you like, e.g. 1024x768, RGB = 3 bytes per pixel +// auto pixels = new unsigned char[1024*768*3]; +// => you need to define a callback that receives the compressed data byte-by-byte from my JPEG writer +// void myOutput(unsigned char oneByte) { fputc(oneByte, myFileHandle); } // save byte to file +// => let's go ! +// TooJpeg::writeJpeg(myOutput, mypixels, 1024, 768); +#pragma once +namespace TooJpeg +{ + // write one byte (to disk, memory, ...) + typedef void (*WRITE_ONE_BYTE)(unsigned char); + // this callback is called for every byte generated by the encoder and behaves similar to fputc + // if you prefer stylish C++11 syntax then it can be a lambda, too: + // auto myOutput = [](unsigned char oneByte) { fputc(oneByte, output); }; + // output - callback that stores a single byte (writes to disk, memory, ...) + // pixels - stored in RGB format or grayscale, stored from upper-left to lower-right + // width,height - image size + // isRGB - true if RGB format (3 bytes per pixel); false if grayscale (1 byte per pixel) + // quality - between 1 (worst) and 100 (best) + // downsample - if true then YCbCr 4:2:0 format is used (smaller size, minor quality loss) instead of 4:4:4, not relevant for grayscale + // comment - optional JPEG comment (0/NULL if no comment), must not contain ASCII code 0xFF + bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels, unsigned short width, unsigned short height, + bool isRGB = true, unsigned char quality = 90, bool downsample = false, const char* comment = nullptr); +} // namespace TooJpeg +// My main inspiration was Jon Olick's Minimalistic JPEG writer +// ( https://www.jonolick.com/code.html => direct link is https://www.jonolick.com/uploads/7/9/2/1/7921194/jo_jpeg.cpp ). +// However, his code documentation is quite sparse - probably because it wasn't written from scratch and is (quote:) "based on a javascript jpeg writer", +// most likely Andreas Ritter's code: https://github.com/eugeneware/jpeg-js/blob/master/lib/encoder.js +// +// Therefore I wrote the whole lib from scratch and tried hard to add tons of comments to my code, especially describing where all those magic numbers come from. +// And I managed to remove the need for any external includes ... +// yes, that's right: my library has no (!) includes at all, not even #include +// Depending on your callback WRITE_ONE_BYTE, the library writes either to disk, or in-memory, or wherever you wish. +// Moreover, no dynamic memory allocations are performed, just a few bytes on the stack. +// +// In contrast to Jon's code, compression can be significantly improved in many use cases: +// a) grayscale JPEG images need just a single Y channel, no need to save the superfluous Cb + Cr channels +// b) YCbCr 4:2:0 downsampling is often about 20% more efficient (=smaller) than the default YCbCr 4:4:4 with only little visual loss +// +// TooJpeg 1.2+ compresses about twice as fast as jo_jpeg (and about half as fast as libjpeg-turbo). +// A few benchmark numbers can be found on my website https://create.stephan-brumme.com/toojpeg/#benchmark +// +// Last but not least you can optionally add a JPEG comment. +// +// Your C++ compiler needs to support a reasonable subset of C++11 (g++ 4.7 or Visual C++ 2013 are sufficient). +// I haven't tested the code on big-endian systems or anything that smells like an apple. +// +// USE AT YOUR OWN RISK. Because you are a brave soul :-) \ No newline at end of file diff --git a/src/eez/modules/mcu/button.cpp b/src/eez/modules/mcu/button.cpp index 30b27c27d..61cff3d25 100644 --- a/src/eez/modules/mcu/button.cpp +++ b/src/eez/modules/mcu/button.cpp @@ -23,17 +23,20 @@ #include #define CONF_DEBOUNCE_THRESHOLD_TIME 10 // 10 ms +#define CONF_GUI_LONG_TOUCH_TIMEOUT 1000 // 1 sec namespace eez { namespace mcu { -Button::Button(GPIO_TypeDef* port, uint16_t pin, bool activeLow) +Button::Button(GPIO_TypeDef* port, uint16_t pin, bool activeLow, bool detectLongPress) : m_port(port) , m_pin(pin) , m_activePinState(activeLow ? GPIO_PIN_RESET : GPIO_PIN_SET) , m_buttonPinState(activeLow ? GPIO_PIN_RESET : GPIO_PIN_SET) + , m_detectLongPress(detectLongPress) , m_buttonPinStateChangedTick(0) , m_btnIsDown(false) + , m_longPressDetected(false) { } @@ -47,21 +50,24 @@ bool Button::isClicked() { } else { int32_t diff = millis() - m_buttonPinStateChangedTick; if (diff > CONF_DEBOUNCE_THRESHOLD_TIME) { + m_buttonPinStateChangedTick = millis(); m_btnIsDown = true; - return true; + m_longPressDetected = false; + return m_detectLongPress ? false : true; } } } } else { // button is UP if (m_btnIsDown) { - if (m_buttonPinState) { - m_buttonPinState = m_activePinState; + if (m_buttonPinState == m_activePinState) { + m_buttonPinState = m_activePinState == GPIO_PIN_RESET ? GPIO_PIN_SET : GPIO_PIN_RESET; m_buttonPinStateChangedTick = millis(); } else { int32_t diff = millis() - m_buttonPinStateChangedTick; if (diff > CONF_DEBOUNCE_THRESHOLD_TIME) { m_btnIsDown = false; + return m_detectLongPress && !m_longPressDetected ? true : false; } } } @@ -69,5 +75,16 @@ bool Button::isClicked() { return false; } +bool Button::isLongPress() { + if (m_btnIsDown && !m_longPressDetected) { + int32_t diff = millis() - m_buttonPinStateChangedTick; + if (diff > CONF_GUI_LONG_TOUCH_TIMEOUT) { + m_longPressDetected = true; + return true; + } + } + return false; +} + } // namespace mcu } // namespace eez diff --git a/src/eez/modules/mcu/button.h b/src/eez/modules/mcu/button.h index 55df01f4a..221e3abc9 100644 --- a/src/eez/modules/mcu/button.h +++ b/src/eez/modules/mcu/button.h @@ -25,18 +25,21 @@ namespace mcu { class Button { public: - Button(GPIO_TypeDef* port, uint16_t pin, bool activeLow); + Button(GPIO_TypeDef* port, uint16_t pin, bool activeLow, bool detectLongPress); bool isClicked(); + bool isLongPress(); + private: GPIO_TypeDef* m_port; uint16_t m_pin; GPIO_PinState m_activePinState; - GPIO_PinState m_buttonPinState; + bool m_detectLongPress; uint32_t m_buttonPinStateChangedTick; bool m_btnIsDown; + bool m_longPressDetected; }; } // namespace mcu diff --git a/src/eez/modules/mcu/display.h b/src/eez/modules/mcu/display.h index 73be3f63e..2c23ef42e 100644 --- a/src/eez/modules/mcu/display.h +++ b/src/eez/modules/mcu/display.h @@ -78,9 +78,7 @@ uint16_t getBackColor(); uint8_t setOpacity(uint8_t opacity); uint8_t getOpacity(); -void screanshotBegin(); -bool screanshotGetLine(uint8_t *line); -void screanshotEnd(); +const uint8_t * takeScreenshot(); void drawPixel(int x, int y); void drawRect(int x1, int y1, int x2, int y2); diff --git a/src/eez/modules/mcu/encoder.cpp b/src/eez/modules/mcu/encoder.cpp index e66e3e0d3..0ade3a930 100644 --- a/src/eez/modules/mcu/encoder.cpp +++ b/src/eez/modules/mcu/encoder.cpp @@ -49,7 +49,7 @@ namespace encoder { #endif #if defined(EEZ_PLATFORM_STM32) -static Button g_encoderSwitch(ENC_SW_GPIO_Port, ENC_SW_Pin, true); +static Button g_encoderSwitch(ENC_SW_GPIO_Port, ENC_SW_Pin, true, false); static uint16_t g_lastCounter; #endif @@ -228,4 +228,4 @@ float increment(gui::data::Value value, int counter, float min, float max, int c #endif -#endif \ No newline at end of file +#endif diff --git a/src/eez/modules/mcu/simulator/display.cpp b/src/eez/modules/mcu/simulator/display.cpp index 2d4dd9114..e9ee66274 100644 --- a/src/eez/modules/mcu/simulator/display.cpp +++ b/src/eez/modules/mcu/simulator/display.cpp @@ -67,6 +67,7 @@ static uint32_t *g_frontPanelBuffer2; static uint32_t *g_frontPanelBuffer3; static uint8_t *g_screenshotBuffer; +static bool g_takeScreenshot; static int g_screenshotY; //////////////////////////////////////////////////////////////////////////////// @@ -307,6 +308,31 @@ void sync() { return; } + if (g_takeScreenshot) { + g_screenshotBuffer = new uint8_t[480 * 272 * 3]; + + uint8_t *src = (uint8_t *)(g_frontPanelBuffer + g_psuAppContext.y * g_frontPanelWidth + g_psuAppContext.x); + uint8_t *dst = g_screenshotBuffer; + + int srcAdvance = (g_frontPanelWidth - 480) * 4; + + for (int y = 0; y < 272; y++) { + for (int x = 0; x < 480; x++) { + uint8_t b = *src++; + uint8_t g = *src++; + uint8_t r = *src++; + src++; + + *dst++ = r; + *dst++ = g; + *dst++ = b; + } + src += srcAdvance; + } + + g_takeScreenshot = false; + } + if (g_painted) { g_painted = false; @@ -318,6 +344,7 @@ void sync() { g_frontPanelBuffer = g_frontPanelBuffer1; } } + } void finishAnimation() { @@ -342,39 +369,13 @@ int getDisplayHeight() { //////////////////////////////////////////////////////////////////////////////// -void screanshotBegin() { - g_screenshotBuffer = new uint8_t[480 * 272 * 3]; - - uint8_t *src = (uint8_t *)(g_frontPanelBuffer + g_psuAppContext.y * g_frontPanelWidth + g_psuAppContext.x); - uint8_t *dst = g_screenshotBuffer; - - int srcAdvance = (g_frontPanelWidth - 480) * 4; - - for (int y = 0; y < 272; y++) { - for (int x = 0; x < 480; x++) { - *dst++ = *src++; - *dst++ = *src++; - *dst++ = *src++; - src++; - } - src += srcAdvance; - } - - g_screenshotY = 272 - 1; -} - -bool screanshotGetLine(uint8_t *line) { - if (g_screenshotY < 0) { - return false; - } - memcpy(line, g_screenshotBuffer + g_screenshotY * 480 * 3, 480 * 3); - --g_screenshotY; - return true; -} +const uint8_t *takeScreenshot() { + g_takeScreenshot = true; + do { + osDelay(0); + } while (g_takeScreenshot); -void screanshotEnd() { - delete g_screenshotBuffer; - g_screenshotBuffer = nullptr; + return g_screenshotBuffer; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/eez/modules/mcu/stm32/display.cpp b/src/eez/modules/mcu/stm32/display.cpp index c3c0104f7..df22fcb68 100644 --- a/src/eez/modules/mcu/stm32/display.cpp +++ b/src/eez/modules/mcu/stm32/display.cpp @@ -69,7 +69,6 @@ static uint16_t g_buffer[480 * 272]; #endif static bool g_takeScreenshot; -static int g_screenshotY; //////////////////////////////////////////////////////////////////////////////// @@ -79,6 +78,10 @@ uint32_t vramOffset(uint16_t *vram, int x, int y) { return (uint32_t)(vram + y * DISPLAY_WIDTH + x); } +uint32_t vramOffsetRGB888(uint8_t *vram, int x, int y) { + return (uint32_t)(vram + (y * DISPLAY_WIDTH + x) * 3); +} + uint32_t vramOffset(uint32_t *vram, int x, int y) { return (uint32_t)(vram + y * DISPLAY_WIDTH + x); } @@ -126,14 +129,13 @@ void fillRect(uint16_t *dst, int x, int y, int width, int height, uint16_t color hdma2d.Init.OutputOffset = DISPLAY_WIDTH - width; hdma2d.LayerCfg[0].InputOffset = DISPLAY_WIDTH - width; - hdma2d.LayerCfg[0].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[0].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[0].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[0].InputAlpha = 0; hdma2d.LayerCfg[1].InputOffset = DISPLAY_WIDTH - width; - hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_ARGB8888; + hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_ARGB8888; hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; - hdma2d.LayerCfg[1].AlphaInverted = DMA2D_REGULAR_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0; auto dstOffset = vramOffset(dst, x, y); @@ -158,18 +160,17 @@ void bitBlt(void *src, int srcBpp, uint32_t srcLineOffset, uint16_t *dst, int x, if (srcBpp == 32) { hdma2d.LayerCfg[0].InputOffset = DISPLAY_WIDTH - width; - hdma2d.LayerCfg[0].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[0].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[0].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[0].InputAlpha = 0; hdma2d.LayerCfg[1].InputOffset = srcLineOffset; - hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_ARGB8888; + hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_ARGB8888; hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; - hdma2d.LayerCfg[1].AlphaInverted = DMA2D_REGULAR_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0; } else { hdma2d.LayerCfg[1].InputOffset = srcLineOffset; - hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0; } @@ -198,7 +199,7 @@ void bitBlt(uint16_t *src, uint16_t *dst, int x, int y, int width, int height) { hdma2d.Init.OutputOffset = DISPLAY_WIDTH - width; hdma2d.LayerCfg[1].InputOffset = DISPLAY_WIDTH - width; - hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0; @@ -206,7 +207,27 @@ void bitBlt(uint16_t *src, uint16_t *dst, int x, int y, int width, int height) { HAL_DMA2D_Init(&hdma2d); HAL_DMA2D_ConfigLayer(&hdma2d, 1); - HAL_DMA2D_Start(&hdma2d, (uint32_t)vramOffset(src, x, y), vramOffset(dst, x, y), width, height); + HAL_DMA2D_Start(&hdma2d, vramOffset(src, x, y), vramOffset(dst, x, y), width, height); +} + +void bitBltRGB888(uint16_t *src, uint8_t *dst, int x, int y, int width, int height) { + hdma2d.Init.Mode = DMA2D_M2M_PFC; + hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB888; + hdma2d.Init.OutputOffset = DISPLAY_WIDTH - width; + hdma2d.Init.RedBlueSwap = DMA2D_RB_SWAP; + + hdma2d.LayerCfg[1].InputOffset = DISPLAY_WIDTH - width; + hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565; + hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; + hdma2d.LayerCfg[1].InputAlpha = 0; + + DMA2D_WAIT; + + HAL_DMA2D_Init(&hdma2d); + HAL_DMA2D_ConfigLayer(&hdma2d, 1); + HAL_DMA2D_Start(&hdma2d, vramOffset(src, x, y), vramOffsetRGB888(dst, x, y), width, height); + + hdma2d.Init.RedBlueSwap = DMA2D_RB_REGULAR; } void bitBlt(void *src, void *dst, int x1, int y1, int x2, int y2) { @@ -219,7 +240,7 @@ void bitBlt(uint16_t *src, uint16_t *dst, int x, int y, int width, int height, i hdma2d.Init.OutputOffset = DISPLAY_WIDTH - width; hdma2d.LayerCfg[1].InputOffset = DISPLAY_WIDTH - width; - hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0; @@ -227,7 +248,7 @@ void bitBlt(uint16_t *src, uint16_t *dst, int x, int y, int width, int height, i HAL_DMA2D_Init(&hdma2d); HAL_DMA2D_ConfigLayer(&hdma2d, 1); - HAL_DMA2D_Start(&hdma2d, (uint32_t)vramOffset(src, x, y), vramOffset(dst, dstx, dsty), width, height); + HAL_DMA2D_Start(&hdma2d, vramOffset(src, x, y), vramOffset(dst, dstx, dsty), width, height); } void bitBlt(void *src, void *dst, int sx, int sy, int sw, int sh, int dx, int dy, uint8_t opacity) { @@ -244,7 +265,7 @@ void bitBlt(void *src, void *dst, int sx, int sy, int sw, int sh, int dx, int dy hdma2d.Init.OutputOffset = DISPLAY_WIDTH - sw; hdma2d.LayerCfg[1].InputOffset = DISPLAY_WIDTH - sw; - hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0; @@ -259,15 +280,13 @@ void bitBlt(void *src, void *dst, int sx, int sy, int sw, int sh, int dx, int dy hdma2d.Init.OutputOffset = DISPLAY_WIDTH - sw; hdma2d.LayerCfg[0].InputOffset = DISPLAY_WIDTH - sw; - hdma2d.LayerCfg[0].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[0].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[0].AlphaMode = DMA2D_COMBINE_ALPHA; - hdma2d.LayerCfg[0].AlphaInverted = DMA2D_REGULAR_ALPHA; hdma2d.LayerCfg[0].InputAlpha = 0xFF; hdma2d.LayerCfg[1].InputOffset = DISPLAY_WIDTH - sw; - hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[1].AlphaMode = DMA2D_COMBINE_ALPHA; - hdma2d.LayerCfg[1].AlphaInverted = DMA2D_REGULAR_ALPHA; hdma2d.LayerCfg[1].InputAlpha = opacity; DMA2D_WAIT; @@ -287,7 +306,7 @@ void bitBltA8Init(uint16_t color) { hdma2d.Init.OutputOffset = 0; // background - hdma2d.LayerCfg[0].InputColorMode = DMA2D_OUTPUT_RGB565; + hdma2d.LayerCfg[0].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[0].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[0].InputAlpha = 0; hdma2d.LayerCfg[0].InputOffset = 0; @@ -351,6 +370,8 @@ void turnOn() { for (int bufferIndex = 0; bufferIndex < NUM_BUFFERS; bufferIndex++) { g_buffers[bufferIndex].bufferPointer = (uint16_t *)(VRAM_BUFFER5_START_ADDRESS + bufferIndex * VRAM_BUFFER_SIZE); } + + assert((uint32_t)g_buffers[NUM_BUFFERS - 1].bufferPointer + VRAM_BUFFER_SIZE - SDRAM_START_ADDRESS <= SDRAM_SIZE); #endif fillRect(g_buffer, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, 0); @@ -477,7 +498,7 @@ void sync() { if (g_takeScreenshot) { #if OPTION_SDRAM - bitBlt(g_bufferOld, (uint16_t *)VRAM_SCREENSHOOT_BUFFER_START_ADDRESS, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); + bitBltRGB888(g_bufferOld, (uint8_t *)VRAM_SCREENSHOOT_BUFFER_START_ADDRESS, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); DMA2D_WAIT; #endif g_takeScreenshot = false; @@ -500,38 +521,13 @@ int getDisplayHeight() { //////////////////////////////////////////////////////////////////////////////// -void screanshotBegin() { +const uint8_t *takeScreenshot() { g_takeScreenshot = true; do { osDelay(0); } while (g_takeScreenshot); - g_screenshotY = 272 - 1; -} - -bool screanshotGetLine(uint8_t *line) { - if (g_screenshotY < 0) { - return false; - } - - uint16_t *src = ((uint16_t *)VRAM_SCREENSHOOT_BUFFER_START_ADDRESS) + g_screenshotY * getDisplayWidth(); - uint8_t *dst = line; - - int n = 480; - while (n--) { - uint16_t color = *src++; - - *dst++ = COLOR_TO_B(color); - *dst++ = COLOR_TO_G(color); - *dst++ = COLOR_TO_R(color); - } - - --g_screenshotY; - - return true; -} - -void screanshotEnd() { + return (const uint8_t *)VRAM_SCREENSHOOT_BUFFER_START_ADDRESS; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/eez/modules/psu/event_queue.cpp b/src/eez/modules/psu/event_queue.cpp index 1a494b898..085a6829b 100644 --- a/src/eez/modules/psu/event_queue.cpp +++ b/src/eez/modules/psu/event_queue.cpp @@ -100,8 +100,8 @@ void doPushEvent(int16_t eventId) { } int eventType = getEventType(&e); - if (eventType == EVENT_TYPE_ERROR || - (eventType == EVENT_TYPE_WARNING && g_eventQueue.lastErrorEventIndex == NULL_INDEX)) { + int lastEventType = getEventType(getLastErrorEvent()); + if (eventType >= lastEventType) { g_eventQueue.lastErrorEventIndex = g_eventQueue.head; } @@ -138,6 +138,9 @@ Event *getLastErrorEvent() { } int getEventType(Event *e) { + if (!e) { + return EVENT_TYPE_NONE; + } if (e->eventId >= EVENT_INFO_START_ID) { return EVENT_TYPE_INFO; } else if (e->eventId >= EVENT_WARNING_START_ID) { diff --git a/src/eez/modules/psu/event_queue.h b/src/eez/modules/psu/event_queue.h index 2509f3ed1..d00f08fbf 100644 --- a/src/eez/modules/psu/event_queue.h +++ b/src/eez/modules/psu/event_queue.h @@ -244,7 +244,8 @@ static const int EVENT_TYPE_ERROR = 3; EVENT_INFO(FILE_UPLOAD_SUCCEEDED, 123, "File upload succeeded") \ EVENT_INFO(FILE_DOWNLOAD_SUCCEEDED, 124, "File download succeeded") \ EVENT_INFO(COUPLED_IN_COMMON_GND, 125, "Coupled in common GND") \ - EVENT_INFO(COUPLED_IN_SPLIT_RAILS, 126, "Coupled in split rails") + EVENT_INFO(COUPLED_IN_SPLIT_RAILS, 126, "Coupled in split rails") \ + EVENT_INFO(SCREENSHOT_SAVED, 127, "A screenshot was saved") #define EVENT_ERROR_START_ID 10000 #define EVENT_WARNING_START_ID 12000 diff --git a/src/eez/modules/psu/gui/data.cpp b/src/eez/modules/psu/gui/data.cpp index 898a57952..dc30be9d7 100644 --- a/src/eez/modules/psu/gui/data.cpp +++ b/src/eez/modules/psu/gui/data.cpp @@ -181,6 +181,18 @@ EnumItem g_moduleTypeEnumDefinition[] = { { MODULE_TYPE_NONE, "None" }, { 0, 0 } }; #endif +EnumItem g_userSwitchActionEnumDefinition[] = { + { persist_conf::USER_SWITCH_ACTION_NONE, "None" }, + { persist_conf::USER_SWITCH_ACTION_ENCODER_STEP, "Encoder Step" }, + { persist_conf::USER_SWITCH_ACTION_SCREENSHOT, "Screenshot" }, + { persist_conf::USER_SWITCH_ACTION_MANUAL_TRIGGER, "Manual Trigger" }, + { persist_conf::USER_SWITCH_ACTION_OUTPUT_ENABLE, "Output Enable" }, + { persist_conf::USER_SWITCH_ACTION_HOME, "Home/Back" }, + { persist_conf::USER_SWITCH_ACTION_INHIBIT, "Inhibit" }, + //{ persist_conf::USER_SWITCH_ACTION_SELECTED_ACTION, "Selected Action" }, + { 0, 0 } +}; + //////////////////////////////////////////////////////////////////////////////// Value MakeValue(float value, Unit unit) { @@ -3876,17 +3888,19 @@ void data_io_pins(data::DataOperationEnum operation, data::Cursor &cursor, data: void data_io_pins_inhibit_state(data::DataOperationEnum operation, data::Cursor &cursor, data::Value &value) { if (operation == data::DATA_OPERATION_GET) { - const persist_conf::IOPin &inputPin1 = persist_conf::devConf.ioPins[0]; - const persist_conf::IOPin &inputPin2 = persist_conf::devConf.ioPins[1]; - if (inputPin1.function == io_pins::FUNCTION_INHIBIT || inputPin2.function == io_pins::FUNCTION_INHIBIT) { - value = io_pins::isInhibited(); + if (io_pins::isInhibited()) { + value = 1; } else { - value = 2; + const persist_conf::IOPin &inputPin1 = persist_conf::devConf.ioPins[0]; + const persist_conf::IOPin &inputPin2 = persist_conf::devConf.ioPins[1]; + if (inputPin1.function == io_pins::FUNCTION_INHIBIT || inputPin2.function == io_pins::FUNCTION_INHIBIT) { + value = 0; + } else { + value = 2; + } } } else if (operation == data::DATA_OPERATION_IS_BLINKING) { - const persist_conf::IOPin &inputPin1 = persist_conf::devConf.ioPins[0]; - const persist_conf::IOPin &inputPin2 = persist_conf::devConf.ioPins[1]; - value = (inputPin1.function == io_pins::FUNCTION_INHIBIT || inputPin2.function == io_pins::FUNCTION_INHIBIT) && io_pins::isInhibited(); + value = io_pins::isInhibited(); } } diff --git a/src/eez/modules/psu/gui/data.h b/src/eez/modules/psu/gui/data.h index 76d000eda..dadd1e2cf 100644 --- a/src/eez/modules/psu/gui/data.h +++ b/src/eez/modules/psu/gui/data.h @@ -21,9 +21,10 @@ enum EnumDefinition { ENUM_DEFINITION_IO_PINS_INPUT_FUNCTION, ENUM_DEFINITION_IO_PINS_OUTPUT_FUNCTION, ENUM_DEFINITION_SERIAL_PARITY, - ENUM_DEFINITION_DST_RULE + ENUM_DEFINITION_DST_RULE, + ENUM_DEFINITION_USER_SWITCH_ACTION, #if defined(EEZ_PLATFORM_SIMULATOR) - , ENUM_DEFINITION_MODULE_TYPE + ENUM_DEFINITION_MODULE_TYPE #endif }; @@ -38,7 +39,7 @@ extern EnumItem g_ioPinsInputFunctionEnumDefinition[]; extern EnumItem g_ioPinsOutputFunctionEnumDefinition[]; extern EnumItem g_serialParityEnumDefinition[]; extern EnumItem g_dstRuleEnumDefinition[]; - +extern EnumItem g_userSwitchActionEnumDefinition[]; #if defined(EEZ_PLATFORM_SIMULATOR) extern EnumItem g_moduleTypeEnumDefinition[]; #endif diff --git a/src/eez/modules/psu/gui/psu.cpp b/src/eez/modules/psu/gui/psu.cpp index 577e13013..4359d5b11 100644 --- a/src/eez/modules/psu/gui/psu.cpp +++ b/src/eez/modules/psu/gui/psu.cpp @@ -100,7 +100,7 @@ Channel *g_channel; static WidgetCursor g_toggleOutputWidgetCursor; #if EEZ_PLATFORM_STM32 -static mcu::Button g_userSwitch(USER_SW_GPIO_Port, USER_SW_Pin, true); +static mcu::Button g_userSwitch(USER_SW_GPIO_Port, USER_SW_Pin, true, true); #endif bool showSetupWizardQuestion(); @@ -192,7 +192,9 @@ void PsuAppContext::stateManagment() { } #if EEZ_PLATFORM_STM32 - if (g_userSwitch.isClicked()) { + if (g_userSwitch.isLongPress()) { + action_select_user_switch_action(); + } else if (g_userSwitch.isClicked()) { action_user_switch_clicked(); } #endif diff --git a/src/eez/modules/psu/io_pins.cpp b/src/eez/modules/psu/io_pins.cpp index 03610f092..3e5d1d1d3 100644 --- a/src/eez/modules/psu/io_pins.cpp +++ b/src/eez/modules/psu/io_pins.cpp @@ -262,8 +262,18 @@ void refresh() { } } +static bool g_isInhibitedByUser; + +bool getIsInhibitedByUser() { + return g_isInhibitedByUser; +} + +void setIsInhibitedByUser(bool isInhibitedByUser) { + g_isInhibitedByUser = isInhibitedByUser; +} + bool isInhibited() { - return g_lastState.inhibited ? true : false; + return g_isInhibitedByUser || g_lastState.inhibited ? true : false; } void setPinState(int pin, bool state) { diff --git a/src/eez/modules/psu/io_pins.h b/src/eez/modules/psu/io_pins.h index 8861f7b79..4a8689fda 100644 --- a/src/eez/modules/psu/io_pins.h +++ b/src/eez/modules/psu/io_pins.h @@ -58,6 +58,9 @@ bool getPinState(int pin); int ioPinRead(int pin); void ioPinWrite(int pin, int state); +bool getIsInhibitedByUser(); +void setIsInhibitedByUser(bool isInhibitedByUser); + } } } // namespace eez::psu::io_pins \ No newline at end of file diff --git a/src/eez/modules/psu/persist_conf.cpp b/src/eez/modules/psu/persist_conf.cpp index 04247c9f5..5c2fe188e 100644 --- a/src/eez/modules/psu/persist_conf.cpp +++ b/src/eez/modules/psu/persist_conf.cpp @@ -90,7 +90,8 @@ static DevConfBlock g_devConfBlocks[] = { { offsetof(DeviceConfiguration, serialBaud), 1, false, 0, 0, 0 }, { offsetof(DeviceConfiguration, triggerSource), 1, false, 0, 0, 0 }, { offsetof(DeviceConfiguration, ytGraphUpdateMethod), 1, false, 0, 0, 0 }, - { sizeof(DeviceConfiguration), 1, false, 0, 60 * 1000, 0 } + { offsetof(DeviceConfiguration, userSwitchAction), 1, false, 0, 60 * 1000, 0 }, + { sizeof(DeviceConfiguration), 1, false, 0, 0, 0 }, }; static struct { @@ -176,6 +177,9 @@ void initDefaultDevConf() { g_defaultDevConf.channelsViewModeInMax = 0; g_defaultDevConf.ytGraphUpdateMethod = YT_GRAPH_UPDATE_METHOD_SCROLL; + +// block 7 + g_defaultDevConf.userSwitchAction = USER_SWITCH_ACTION_ENCODER_STEP; }; //////////////////////////////////////////////////////////////////////////////// @@ -344,7 +348,7 @@ static const unsigned PERSISTENT_STORAGE_ADDRESS_ALIGNMENT = 32; void init() { initDefaultDevConf(); - bool storageInitialized = true; + //bool storageInitialized = true; uint8_t blockData[sizeof(BlockHeader) + sizeof(DeviceConfiguration)]; uint16_t blockAddress = PERSIST_CONF_DEV_CONF_ADDRESS; @@ -356,13 +360,13 @@ void init() { if (!confRead(blockData, blockStorageSize, blockAddress, g_devConfBlocks[i].version)) { if (!confRead(blockData, blockStorageSize, blockAddress + blockStorageSize, g_devConfBlocks[i].version)) { - if (i == 0) { - // if we can't read first block at both locations we assume storage is not initialized - storageInitialized = false; - } else if (storageInitialized) { - // generate error only if storage is intialized and we failed to read block at both locations - pushEvent(event_queue::EVENT_ERROR_EEPROM_MCU_CRC_CHECK_ERROR); - } + //if (i == 0) { + // // if we can't read first block at both locations we assume storage is not initialized + // storageInitialized = false; + //} else if (storageInitialized) { + // // generate error only if storage is intialized and we failed to read block at both locations + // pushEvent(event_queue::EVENT_ERROR_EEPROM_MCU_CRC_CHECK_ERROR); + //} // use default g_devConf data for this block memcpy(blockData + sizeof(BlockHeader), (uint8_t *)&g_defaultDevConf + blockStart, blockSize); @@ -415,12 +419,10 @@ void tick() { g_devConfBlocks[i].dirty = memcmp((uint8_t *)&devConf + blockStart, (uint8_t *)&g_savedDevConf + blockStart, blockSize) != 0; } - int32_t diff = tickCountMillis - g_devConfBlocks[i].lastSaveTickCount; - if ( g_devConfBlocks[i].dirty && g_devConfBlocks[i].numSaveErrors < CONF_MAX_NUMBER_OF_SAVE_ERRORS_ALLOWED && - int32_t(tickCountMillis - g_devConfBlocks[i].lastSaveTickCount) >= g_devConfBlocks[i].minTickCountsBetweenSaves + tickCountMillis - g_devConfBlocks[i].lastSaveTickCount >= g_devConfBlocks[i].minTickCountsBetweenSaves ) { memset(blockData, 0, blockStorageSize); memcpy(blockData + sizeof(BlockHeader), (uint8_t *)&devConf + blockStart, blockSize); @@ -1248,6 +1250,10 @@ void setSkipEthernetSetup(unsigned skipEthernetSetup) { g_devConf.skipEthernetSetup = skipEthernetSetup; } +void setUserSwitchAction(UserSwitchAction userSwitchAction) { + g_devConf.userSwitchAction = userSwitchAction; +} + //////////////////////////////////////////////////////////////////////////////// static const uint16_t MODULE_PERSIST_CONF_BLOCK_MODULE_CONFIGURATION_ADDRESS = 64; diff --git a/src/eez/modules/psu/persist_conf.h b/src/eez/modules/psu/persist_conf.h index 6b953507b..2d09d5b52 100644 --- a/src/eez/modules/psu/persist_conf.h +++ b/src/eez/modules/psu/persist_conf.h @@ -61,6 +61,17 @@ struct IOPin { unsigned function : 7; }; +enum UserSwitchAction { + USER_SWITCH_ACTION_NONE, + USER_SWITCH_ACTION_ENCODER_STEP, + USER_SWITCH_ACTION_SCREENSHOT, + USER_SWITCH_ACTION_MANUAL_TRIGGER, + USER_SWITCH_ACTION_OUTPUT_ENABLE, + USER_SWITCH_ACTION_HOME, + USER_SWITCH_ACTION_INHIBIT, + USER_SWITCH_ACTION_SELECTED_ACTION +}; + /// Device configuration block. struct DeviceConfiguration { // block 1 @@ -156,6 +167,10 @@ struct DeviceConfiguration { unsigned channelsViewMode : 3; unsigned maxChannel : 3; // 0: default view, 1: Ch1 maxed, 2: Ch2 maxed, ... unsigned channelsViewModeInMax : 3; + + // block 7 + UserSwitchAction userSwitchAction; + uint8_t reserved[60]; }; extern const DeviceConfiguration &devConf; @@ -289,6 +304,8 @@ void setSkipDateTimeSetup(unsigned skipDateTimeSetup); void setSkipSerialSetup(unsigned skipSerialSetup); void setSkipEthernetSetup(unsigned skipEthernetSetup); +void setUserSwitchAction(UserSwitchAction userSwitchAction); + //////////////////////////////////////////////////////////////////////////////// struct ModuleConfiguration { diff --git a/src/eez/modules/psu/scpi/display.cpp b/src/eez/modules/psu/scpi/display.cpp index d2f1f3156..00345a612 100644 --- a/src/eez/modules/psu/scpi/display.cpp +++ b/src/eez/modules/psu/scpi/display.cpp @@ -28,6 +28,8 @@ #include #endif +#include + namespace eez { namespace psu { namespace scpi { @@ -174,57 +176,26 @@ scpi_result_t scpi_cmd_displayWindowTextClear(scpi_t *context) { scpi_result_t scpi_cmd_displayDataQ(scpi_t *context) { // TODO migrate to generic firmware #if OPTION_DISPLAY - const uint8_t buffer[] = { - // BMP Header (14 bytes) - - // ID field (42h, 4Dh): BM - 0x42, 0x4D, - // size of BMP file - 0x36, 0xFA, 0x05, 0x00, // 14 + 40 + (480 * 272 * 3) = 391734‬ = 0x5FA36 - // unused - 0x00, 0x00, - // unused - 0x00, 0x00, - // Offset where the pixel array (bitmap data) can be found - 0x36, 0x00, 0x00, 0x00, // 53 = 0x36 - - // DIB Header (40 bytes) - - // Number of bytes in the DIB header (from this point) - 0x28, 0x00, 0x00, 0x00, // 40 = 0x28 - // Width of the bitmap in pixels - 0xE0, 0x01, 0x00, 0x00, // 480 = 0x1E0 - // Height of the bitmap in pixels. Positive for bottom to top pixel order. - 0x10, 0x01, 0x00, 0x00, // 272 = 0x110 - // Number of color planes being used - 0x01, 0x00, - // Number of bits per pixel - 0x18, 0x00, // 24 = 0x18 - // BI_RGB, no pixel array compression used - 0x00, 0x00, 0x00, 0x00, - // Size of the raw bitmap data (including padding) - 0x00, 0xFA, 0x05, 0x00, // 480 * 272 * 3 = 0x5FA00 - // Print resolution of the image, - // 72 DPI × 39.3701 inches per metre yields 2834.6472 - 0x13, 0x0B, 0x00, 0x00, // 2835 pixels/metre horizontal - 0x13, 0x0B, 0x00, 0x00, // 2835 pixels/metre vertical - // Number of colors in the palette - 0x00, 0x00, 0x00, 0x00, - // 0 means all colors are important - 0x00, 0x00, 0x00, 0x00, - }; - - SCPI_ResultArbitraryBlockHeader(context, sizeof(buffer) + 480 * 272 * 3); - SCPI_ResultArbitraryBlockData(context, buffer, sizeof(buffer)); - - mcu::display::screanshotBegin(); - - uint8_t line[480 * 3]; - while (mcu::display::screanshotGetLine(line)) { - SCPI_ResultArbitraryBlockData(context, line, sizeof(line)); + const uint8_t *screenshotPixels = mcu::display::takeScreenshot(); + + unsigned char* imageData; + size_t imageDataSize; + + if (jpegEncode(screenshotPixels, &imageData, &imageDataSize)) { + SCPI_ErrorPush(context, SCPI_ERROR_OUT_OF_MEMORY_FOR_REQ_OP); + return SCPI_RES_ERR; } - mcu::display::screanshotEnd(); + SCPI_ResultArbitraryBlockHeader(context, imageDataSize); + + static const size_t CHUNK_SIZE = 1024; + + while (imageDataSize > 0) { + size_t n = MIN(imageDataSize, CHUNK_SIZE); + SCPI_ResultArbitraryBlockData(context, imageData, n); + imageData += n; + imageDataSize -= n; + } return SCPI_RES_OK; #else diff --git a/src/eez/platform/simulator/front_panel.cpp b/src/eez/platform/simulator/front_panel.cpp index c5ff9aea7..120a5fdd9 100644 --- a/src/eez/platform/simulator/front_panel.cpp +++ b/src/eez/platform/simulator/front_panel.cpp @@ -56,6 +56,13 @@ int FrontPanelAppContext::getMainPageId() { return PAGE_ID_FRONT_PANEL; } +int FrontPanelAppContext::getLongTouchActionHook(const WidgetCursor &widgetCursor) { + if (widgetCursor.widget->action == ACTION_ID_USER_SWITCH_CLICKED) { + return ACTION_ID_SELECT_USER_SWITCH_ACTION; + } + return ACTION_ID_NONE; +} + //////////////////////////////////////////////////////////////////////////////// void data_main_app_view(DataOperationEnum operation, Cursor &cursor, Value &value) { diff --git a/src/eez/platform/simulator/front_panel.h b/src/eez/platform/simulator/front_panel.h index c6306768c..6155bd504 100644 --- a/src/eez/platform/simulator/front_panel.h +++ b/src/eez/platform/simulator/front_panel.h @@ -26,10 +26,12 @@ namespace eez { namespace gui { class FrontPanelAppContext : public AppContext { - public: +public: FrontPanelAppContext(); - protected: + int getLongTouchActionHook(const WidgetCursor &widgetCursor) override; + +protected: int getMainPageId() override; }; diff --git a/src/eez/platform/stm32/defines.h b/src/eez/platform/stm32/defines.h index d2b75800a..ec6c09212 100644 --- a/src/eez/platform/stm32/defines.h +++ b/src/eez/platform/stm32/defines.h @@ -19,24 +19,26 @@ #pragma once #define SDRAM_START_ADDRESS 0xc0000000u +#define SDRAM_SIZE (8 * 1024 * 1024) #define DISPLAY_WIDTH 480 #define DISPLAY_HEIGHT 272 -#define DISPLAY_BPP 16 - -#define DISPLAY_BYTESPP (DISPLAY_BPP / 8) - #define DECOMPRESSED_ASSETS_START_ADDRESS SDRAM_START_ADDRESS #define DLOG_BUFFER (SDRAM_START_ADDRESS + 3 * 512 * 1024) #define DLOG_BUFFER_SIZE (1024 * 1024) -#define VRAM_BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_HEIGHT * DISPLAY_BYTESPP) +#define VRAM_SCREENSHOOT_JPEG_OUT_BUFFER (DLOG_BUFFER + DLOG_BUFFER_SIZE) +#define VRAM_SCREENSHOOT_JPEG_OUT_BUFFER_SIZE (256 * 1024) +#define VRAM_SCREENSHOOT_BUFFER_START_ADDRESS (VRAM_SCREENSHOOT_JPEG_OUT_BUFFER + VRAM_SCREENSHOOT_JPEG_OUT_BUFFER_SIZE) + +#define VRAM_SCREENSHOOT_BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_HEIGHT * 3) // RGB888 + +#define VRAM_BUFFER1_START_ADDRESS (VRAM_SCREENSHOOT_BUFFER_START_ADDRESS + VRAM_SCREENSHOOT_BUFFER_SIZE) -#define VRAM_SCREENSHOOT_BUFFER_START_ADDRESS (DLOG_BUFFER + DLOG_BUFFER_SIZE) +#define VRAM_BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_HEIGHT * 2) // RGB565 -#define VRAM_BUFFER1_START_ADDRESS (VRAM_SCREENSHOOT_BUFFER_START_ADDRESS + VRAM_BUFFER_SIZE) #define VRAM_BUFFER2_START_ADDRESS (VRAM_BUFFER1_START_ADDRESS + VRAM_BUFFER_SIZE) #define VRAM_BUFFER3_START_ADDRESS (VRAM_BUFFER2_START_ADDRESS + VRAM_BUFFER_SIZE) #define VRAM_BUFFER4_START_ADDRESS (VRAM_BUFFER3_START_ADDRESS + VRAM_BUFFER_SIZE) diff --git a/src/eez/scpi/scpi.cpp b/src/eez/scpi/scpi.cpp index 3d356ddab..213569c49 100644 --- a/src/eez/scpi/scpi.cpp +++ b/src/eez/scpi/scpi.cpp @@ -34,13 +34,17 @@ #include #if OPTION_SD_CARD #include +#include #include +#include #endif #include #include #include +#include #include +#include using namespace eez::psu; using namespace eez::psu::scpi; @@ -57,7 +61,7 @@ osThreadId g_scpiTaskHandle; #pragma GCC diagnostic ignored "-Wwrite-strings" #endif -osThreadDef(g_scpiTask, mainLoop, osPriorityNormal, 0, 4096); +osThreadDef(g_scpiTask, mainLoop, osPriorityNormal, 0, 8192); #if defined(EEZ_PLATFORM_STM32) #pragma GCC diagnostic pop @@ -137,10 +141,12 @@ void oneIter() { if (!sd_card::exists("/recordings", &err)) { if (err != SCPI_ERROR_FILE_NAME_NOT_FOUND) { event_queue::pushEvent(err); - } else { - if (!sd_card::makeDir("/recordings", &err)) { - event_queue::pushEvent(err); - } + return; + } + + if (!sd_card::makeDir("/recordings", &err)) { + event_queue::pushEvent(err); + return; } } @@ -170,9 +176,54 @@ void oneIter() { } } else if (type == SCPI_QUEUE_MESSAGE_ABORT_DOWNLOADING) { abortDownloading(); + } else if (type == SCPI_QUEUE_MESSAGE_SCREENSHOT) { + const uint8_t *screenshotPixels = mcu::display::takeScreenshot(); + + unsigned char* imageData; + size_t imageDataSize; + + if (jpegEncode(screenshotPixels, &imageData, &imageDataSize)) { + event_queue::pushEvent(SCPI_ERROR_OUT_OF_MEMORY_FOR_REQ_OP); + return; + } + + int err; + if (!sd_card::exists("/screenshots", &err)) { + if (err != SCPI_ERROR_FILE_NAME_NOT_FOUND) { + event_queue::pushEvent(err); + return; + } else if (!sd_card::makeDir("/screenshots", &err)) { + event_queue::pushEvent(err); + return; + } + } + + char filePath[40]; + uint8_t year, month, day, hour, minute, second; + datetime::getDate(year, month, day); + datetime::getTime(hour, minute, second); + sprintf(filePath, "/screenshots/%d_%02d_%02d-%02d_%02d_%02d.jpg", + (int)(year + 2000), (int)month, (int)day, + (int)hour, (int)minute, (int)second); + + File file; + if (file.open(filePath, FILE_CREATE_ALWAYS | FILE_WRITE)) { + size_t written = file.write(imageData, imageDataSize); + + file.close(); + + if (written == imageDataSize) { + // success! + event_queue::pushEvent(event_queue::EVENT_INFO_SCREENSHOT_SAVED); + } else { + sd_card::deleteFile(filePath, &err); + event_queue::pushEvent(SCPI_ERROR_MASS_STORAGE_ERROR); + } + } else { + event_queue::pushEvent(SCPI_ERROR_FILE_NAME_NOT_FOUND); + } } #endif - } } else { uint32_t tickCount = micros(); diff --git a/src/eez/scpi/scpi.h b/src/eez/scpi/scpi.h index 3e61bd7b5..8a3099ab0 100644 --- a/src/eez/scpi/scpi.h +++ b/src/eez/scpi/scpi.h @@ -54,6 +54,7 @@ extern osMessageQId g_scpiMessageQueueId; #define SCPI_QUEUE_MESSAGE_DLOG_FILE_WRITE 4 #define SCPI_QUEUE_MESSAGE_DLOG_TOGGLE 5 #define SCPI_QUEUE_MESSAGE_ABORT_DOWNLOADING 6 +#define SCPI_QUEUE_MESSAGE_SCREENSHOT 7 extern char g_listFilePath[CH_MAX][MAX_PATH_LENGTH]; diff --git a/src/eez/scpi/scpi_user_config.h b/src/eez/scpi/scpi_user_config.h index 92d906f89..6362dd32a 100644 --- a/src/eez/scpi/scpi_user_config.h +++ b/src/eez/scpi/scpi_user_config.h @@ -53,6 +53,7 @@ extern "C" { X(SCPI_ERROR_TRIGGER_IGNORED, -211, "Trigger ignored") \ X(SCPI_ERROR_DATA_OUT_OF_RANGE, -222, "Data out of range") \ X(SCPI_ERROR_TOO_MUCH_DATA, -223, "Too much data") \ + X(SCPI_ERROR_OUT_OF_MEMORY_FOR_REQ_OP, -225, "Out of memory") \ X(SCPI_ERROR_DIGITAL_PIN_FUNCTION_MISMATCH, -230, "Digital pin function mismatch") \ X(SCPI_ERROR_HARDWARE_ERROR, -240, "Hardware error") \ X(SCPI_ERROR_HARDWARE_MISSING, -241, "Hardware missing") \ diff --git a/src/third_party/stm32_r1b5/.cproject b/src/third_party/stm32_r1b5/.cproject index 765b33932..38d6869f1 100644 --- a/src/third_party/stm32_r1b5/.cproject +++ b/src/third_party/stm32_r1b5/.cproject @@ -69,6 +69,8 @@ + + @@ -148,6 +156,12 @@ + + + + + + @@ -391,6 +422,13 @@ + + + + + + + @@ -148,6 +154,12 @@ + + + + + + @@ -391,6 +409,12 @@ + + + + + +