Return to Home Page

Knob Listener Observer

The knob listener observer allows the menu drawn by the Pico to be interactive:

Linster OS Menu on BMBT

Implementation

The object-oriented design of the Pico firmware shines here. All the dependencies are passed in, and it’s up to the ScreenManager to keep track of the application state.

//
// Created by stefan on 12/2/22.
//

#include "KnobListenerObserver.h"
#include "fmt/format.h"

namespace pico::ibus::observers {

KnobListenerObserver::KnobListenerObserver(
        std::shared_ptr<logger::BaseLogger> baseLogger,
        std::shared_ptr<video::scanProgram::ScanProgramSwapper> scanProgramSwapper,
        std::shared_ptr<pico::hardware::videoSwitch::VideoSwitch> videoSwitch,
        std::shared_ptr<video::ScreenManager::ScreenManager> screenManager) {
    this->logger = baseLogger;
    this->scanProgramSwapper = scanProgramSwapper;
    if (this->scanProgramSwapper == nullptr) {
        logger->wtf("KnobListenerObserver", "scanProgramSwapper is null");
    }

    this->videoSwitch = videoSwitch;
    if (this->videoSwitch == nullptr) {
        logger->wtf("KnobListenerObserver", "videoSwitch is null");
    }

    this->screenManager = screenManager;
    if (this->screenManager == nullptr) {
        logger->wtf("KnobListenerObserver", "screenManager is null");
    }

}

void KnobListenerObserver::onNewPacket(std::shared_ptr<pico::ibus::data::IbusPacket> iBusPacket) {
    if (iBusPacket->getSourceDevice() == data::IbusDeviceEnum::BOARDMONITOR_BUTTONS &&
     iBusPacket->getDestinationDevice() == data::NAV_VIDEOMODULE) {

        if (iBusPacket->getDataLength() >= 2) {
            //PushData = 0x48, 0x05
            if (iBusPacket->getRawPacket()[data::IbusPacket::DATA_START + 0] == 0x48) {
                if (iBusPacket->getRawPacket()[data::IbusPacket::DATA_START + 1] == 0x05) {
                    onKnobPressed();
                    return;
                }
            }

            if (iBusPacket->getRawPacket()[data::IbusPacket::DATA_START + 0] == 0x49) {
                uint8_t rotation = iBusPacket->getRawPacket()[data::IbusPacket::DATA_START + 1];

                if (rotation - 0x0 >= 1 && rotation - 0x00 <= 9) {
                    onKnobTurnedLeft(rotation);
                    return;
                }

                if (rotation - 0x80 >= 1 && rotation - 0x80 <= 9) {
                    onKnobTurnedRight(rotation - 0x80);
                    return;
                }
            }
        }
    }
}

void KnobListenerObserver::onKnobTurnedLeft(int clicks) {
    logger->d(getTag(), fmt::format("onKnobTurnedLeft, clicks {:d}", clicks));

    if ((videoSwitch->getPreviousVideoSource() == hardware::videoSwitch::VideoSource::PICO &&
         scanProgramSwapper->getCurrentScanProgram() == ScanProgram::MENU
        ) || mock_knob_state_preConditions) {

        logger->d(getTag(), "Dispatching to ScreenManager");

        screenManager->focusPreviousItem(clicks);
    }
    if (debugDraw) {
        screenManager->getCurrentScreen()->drawToLogger(logger);
    }
}

void KnobListenerObserver::onKnobTurnedRight(int clicks) {
    logger->d(getTag(), fmt::format("onKnobTurnedRight, clicks {:d}", clicks));

    if ((videoSwitch->getPreviousVideoSource() == hardware::videoSwitch::VideoSource::PICO &&
         scanProgramSwapper->getCurrentScanProgram() == ScanProgram::MENU
        ) || mock_knob_state_preConditions) {

        logger->d(getTag(), "Dispatching to ScreenManager");

        screenManager->focusNextItem(clicks);
    }
    if (debugDraw) {
        screenManager->getCurrentScreen()->drawToLogger(logger);
    }
}

void KnobListenerObserver::onKnobPressed() {
    logger->d(getTag(), "onKnobPressed");

    if ((videoSwitch->getPreviousVideoSource() == hardware::videoSwitch::VideoSource::PICO &&
        scanProgramSwapper->getCurrentScanProgram() == ScanProgram::MENU
        ) || mock_knob_state_preConditions) {

        logger->d(getTag(), "Dispatching to ScreenManager");

        screenManager->clickOnItem();
    }
}


} // observers

The ScreenManager holds one screen (but can hold more eventually), and delegates to a Screen. The screen focusing logic looks like this:

    void Screen::focusNextItem(int clicks) {
//        logger->d(getTag(), fmt::format("focusNextItem: Current focusedIndex: {}", focusedIndex));
        if (clicks < 0 ) {
//            logger->w(getTag(), "Asked for a negative number for forward clicks");
            return;
        }
        if (clicks == 0) {
            return;
        }
        if (clicks == 1) {
            int oldFocusedIndex = focusedIndex;
            callOnUnFocusItem(oldFocusedIndex);
            focusedIndex = focusedIndex + 1;
            if (focusedIndex == this->getScreenItems().size()) {
                focusedIndex = 0;
            }
            callOnFocusItem(focusedIndex);
//            logger->d(getTag(), fmt::format("focusNextItem: New focusedIndex: {}", focusedIndex));
        }

        if (clicks > 1) {
            int toClick = clicks;
            while (toClick > 0) {
                focusNextItem(--toClick);
            }
        }
    }
    
    ...
    
        void Screen::callOnFocusItem(int index) {
//        logger->d(getTag(), fmt::format("Focusing item with index: {}", index));

        auto item = getScreenItems()[index];

        item->setIsFocused(true);
        item->onItemFocused();

        if (this->debugPrintAsciiArt) {
            drawToLogger(logger);
        }

//        logger->d(getTag(), fmt::format("Focused item with index: {}", index));
    }

    void Screen::callOnUnFocusItem(int index) {
//        logger->d(getTag(), fmt::format("UnFocusing item with index: {}", index));

        auto item = getScreenItems()[index];

        item->setIsFocused(false);
        item->onItemUnfocused();

//        logger->d(getTag(), fmt::format("UnFocused item with index: {}", index));
    }

with a particular ScreenItem being a subclass of:

//
// Created by stefan on 6/18/23.
//

#ifndef PICOTEMPLATE_SCREENITEM_H
#define PICOTEMPLATE_SCREENITEM_H

#include <string>

namespace video::ScreenManager {

        class ScreenItem {

        private:
            bool isFocused = false;
        public:

            virtual std::string getLabel() = 0;

            virtual void onItemFocused() {};
            virtual void onItemUnfocused() { };

            virtual void onItemClicked() = 0;

            bool getIsFocused();
            void setIsFocused(bool isFocused);
        };

    } // screenManager

#endif //PICOTEMPLATE_SCREENITEM_H

which results in the MenuScanProgram drawing a box around the focused screen item.

void MenuScanProgram::drawScreenMenuItems(std::vector<std::shared_ptr<video::ScreenManager::ScreenItem>> screenItems) {

        uint16_t menuItem_tl_y = 75;

        for (const auto item : screenItems) {
            menuItem_tl_y = menuItem_tl_y + drawScreenMenuItem(menuItem_tl_y, item);
        }
    }

    uint16_t MenuScanProgram::drawScreenMenuItem(uint16_t tl, std::shared_ptr<ScreenManager::ScreenItem> item) {

        uint16_t menuItem_x = 30;

        if (item->getIsFocused()) {
            menuGraphicsLib->drawEmptyRectangle(
                    scanVideo::graphics::command::PxCoord(menuItem_x, tl),
                    scanVideo::graphics::command::PxCoord(getDisplayWidthPx() - 58, tl + 8 + 6),
                    menuGraphicsLib->getPalette()[15],
                    2
            );
        }

        menuGraphicsLib->drawText(
                item->getLabel(),
                scanVideo::graphics::command::PxCoord(menuItem_x + 4, tl + 4),
                menuGraphicsLib->getPalette()[15],
                1
        );

        return 8 + 8;
    }
Return to Top