Knob Listener Observer
The knob listener observer allows the menu drawn by the Pico to be interactive:
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;
}