Date post: | 29-Jan-2018 |
Category: |
Engineering |
Upload: | geert-bevin |
View: | 118 times |
Download: | 5 times |
WHAT IS THIS ABOUT?
> Product with a central LCD UI> Physical front-panel with knobs and switches
> Embedded Linux at its core> JUCE as cross-platform framework
(develop on macOS, run on Linux)> We learned a few things we'd like to share
AGENDA
> Hardware GUI design> Embrace the ProJucer
> UI navigation> Instantiate early
> Virtual Panel> Automated tests
TARGET DISPLAY HARDWARE
> Colours, gamma, contrast> Font rendering (OSX v. Linux)
> Refresh rate> Glare and environment
HARD VERSUS SOFT CONTROLS
> Hard as in hardware (fixed-purpose)High frequencyHigh immediacy
> Soft as in software-defined (changeable)Embedded GUI provides soft control and feedback
EMBRACE THE PROJUCER
> Project management done automatically> WYSIWYG UI and graphics editing
> Component-level re-use> Full code-level tweakability
> Embed external binary resources
FULL CODE-LEVEL TWEAKABILITY
OscillatorGraph::OscillatorGraph() { //[Constructor_pre] You can add your own custom stuff here.. mix_ = 0.f; pulseWidth_ = 0.f; mode_ = "F"; //[/Constructor_pre]
//[UserPreSize] //[/UserPreSize]
setSize (380, 280);
//[Constructor] You can add your own custom stuff here.. //[/Constructor]}
FULL CODE-LEVEL TWEAKABILITYvoid OscillatorGraph::resized() { //[UserPreResize] Add your own custom resize code here.. //[/UserPreResize]
internalPath1.clear(); internalPath1.startNewSubPath (19.0f, 257.0f); internalPath1.lineTo (76.0f, 23.0f); internalPath1.lineTo (133.0f, 257.0f); internalPath1.lineTo (190.0f, 23.0f); internalPath1.lineTo (247.0f, 257.0f); internalPath1.lineTo (304.0f, 23.0f); internalPath1.lineTo (361.0f, 257.0f);
//[UserResized] Add your own custom resize handling here.. internalPath1.clear(); internalPath1 = drawOscillator(Rectangle<float>(19.f, 23.f, 342.f, 234.f), mix_, pulseWidth_, mode_); //[/UserResized]}
EMBED EXTERNAL BINARY RESOURCESstruct LASLookAndFeel::Pimpl { Pimpl() : futuraPtBook_(Typeface::createSystemTypefaceFor(BinaryData::FTN45_Futura_PT_Book_otf, BinaryData::FTN45_Futura_PT_Book_otfSize)), futuraPtMedium_(Typeface::createSystemTypefaceFor(BinaryData::FTN55_Futura_PT_Medium_otf, BinaryData::FTN55_Futura_PT_Medium_otfSize)) {}
Typeface::Ptr getTypefaceForFont(const Font& font) { if (font.getTypefaceName() == "Futura PT") { if (font.getTypefaceStyle() == "Heavy") { return futuraPtHeavy_; } else { return futuraPtMedium_; } }
return nullptr; }
Typeface::Ptr futuraPtBook_; Typeface::Ptr futuraPtMedium_;};
EMBED EXTERNAL BINARY RESOURCESstruct LASLookAndFeel::Pimpl { Pimpl() : futuraPtBook_(Typeface::createSystemTypefaceFor(BinaryData::FTN45_Futura_PT_Book_otf, BinaryData::FTN45_Futura_PT_Book_otfSize)), futuraPtMedium_(Typeface::createSystemTypefaceFor(BinaryData::FTN55_Futura_PT_Medium_otf, BinaryData::FTN55_Futura_PT_Medium_otfSize)) {}
Typeface::Ptr getTypefaceForFont(const Font& font) { if (font.getTypefaceName() == "Futura PT") { if (font.getTypefaceStyle() == "Heavy") { return futuraPtHeavy_; } else { return futuraPtMedium_; } }
return nullptr; }
Typeface::Ptr futuraPtBook_; Typeface::Ptr futuraPtMedium_;};
NAVIGATIONAL WIDGETS
> Limitations can be a strength> Keep UI organisation as shallow as possible
> Dedicated buttons are quicker than lists> Minimise long lists - group by related functions
UI NAVIGATION EXAMPLES
> Rotate encoder to scroll or adjust value> Shift-rotate encoder to move selected item
> Press encoder to change UI focus> Shift-press encoder to confirm action
INSTANTIATE EARLY
> Plenty of memory, limited CPU> Rapid dispatch and propagation> Whole UI is always up-to-date> Update data without redraws
PLENTY OF MEMORY, LIMITED CPU
MainComponent::MainComponent() { //[UserPreSize] // create all UI panels lfo1_ = new LFOPanel(0); lfo1_->setComponentID(LFO1); lfo2_ = new LFOPanel(1); lfo2_->setComponentID(LFO2); oscillator1_ = new OscillatorPanel(0); oscillator1_->setComponentID(OSCILLATOR1); oscillator2_ = new OscillatorPanel(1); oscillator2_->setComponentID(OSCILLATOR2); // ... addChildComponent(lfo1_); addChildComponent(lfo2_); addChildComponent(oscillator1_); addChildComponent(oscillator2_); // ... for (int c = 0; c < getNumChildComponents(); ++c) { getChildComponent(c)->setVisible(false); } //[/UserPreSize]
PLENTY OF MEMORY, LIMITED CPU
void MainComponent::showPanel(Component* panel) { if (panel) { Component* parent = panel->getParentComponent(); if (parent) { for (int i = 0; i < parent->getNumChildComponents(); ++i) { Component* child = parent->getChildComponent(i); if (child && child != panel) { child->setVisible(false); } } } panel->setVisible(true); }}
PLENTY OF MEMORY, LIMITED CPU
LFOPanel::LFOPanel(int ordinal) : ordinal_(ordinal) { addAndMakeVisible(rate_ = new Label(String(), TRANS("0.01ms")));
//[UserPreSize] rate_->setComponentID(RATE); //[/UserPreSize] setSize (760, 460);
//[Constructor] You can add your own custom stuff here.. LApp->registerComponentUpdater(lfo1Rate, new LFORateUpdater(this)); //[/Constructor]}
PLENTY OF MEMORY, LIMITED CPU
struct LFORateUpdater : ComponentUpdater {
LFORateUpdater(LFOPanel* panel) : panel_(panel) { };
void update(var value, var previous) override { panel_->updateRateLabel(value); }
LFOPanel* const panel_;};
RAPID DISPATCH AND PROPAGATION
void registerComponentUpdater(UpdaterIndex i, ComponentUpdater* c) { if (componentUpdaterMap_.contains(i)) { ComponentUpdater* updater = componentUpdaterMap_[i]; componentUpdaterMap_.remove(i); delete updater; } if (c != nullptr) componentUpdaterMap_.set(i, c);}void updateComponent(UpdaterIndex i, var value) { if (!lastComponentValues_.contains(i) || lastComponentValues_[i] != value) { var previous = lastComponentValues_[i];
lastComponentValues_.set(i, value);
componentUpdaterMap_[i]->update(value, previous); }}HashMap<UpdaterIndex, ComponentUpdater*> componentUpdaterMap_;
UPDATE DATA WITHOUT REDRAWS
void LFOPanel::updateRateLabel(var rate) { rate_->setText(String(int(rate)) + " Hz", dontSendNotification);}
void LFOPanel::setVisible(bool shouldBeVisible) { rate_->setVisible(shouldBeVisible);
Component::setVisible(shouldBeVisible);}
VIRTUAL PANEL
> Simulated hardware UI> Sends same messages to GUI application
> Ready when your hardware isn’t
AUTOMATED TESTS
> Semantic UI tests> Functional tests and unit tests
> Useful test runner tricks> Hook into Continuous Integration
SEMANTIC UI TESTS
class LFOPanelTests : public GUITest {public: LFOPanelTests() : GUITest("LFOPanelTests") {} void runTest() override { beginGUITest("LFO 1 Visibility"); { // simulate hardware front panel input // ...
expect(getComponentVisibility(IDPATH(LFO1)), "lfo1 not visible"); expect(!getComponentVisibility(IDPATH(LFO2)), "lfo2 visible");
screenshot("lfo1Panel"); } }}
SEMANTIC UI TESTS
class GUITest : public UnitTest { public: Component* findComponent(const StringArray& idPath) const { Component* result = LASFrontEndApplication::getAppInstance()->getMainComponent(); for (String id : idPath) { result = result->findChildWithID(id); if (result == nullptr) return nullptr; } return result; }
bool getComponentVisibility(const StringArray& idPath) const { Component* component = findComponent(idPath); if (nullptr == component) return false;
return component->isVisible(); }
SEMANTIC UI TESTS
beginGUITest("Rate Labels"); { expect(clearLabelText(IDPATH(LFO1,RATE))); expect(clearLabelText(IDPATH(LFO2,RATE)));
// simulate hardware front panel input // ...
expectEquals(getLabelText(IDPATH(LFO1,RATE)), String("0.9 Hz")); expectEquals(getLabelText(IDPATH(LFO2,RATE)), String("100 Hz"));
screenshot("rates"); }
FUNCTIONAL TESTS AND UNIT TESTSclass BitPackedTests : public UnitTest {public: BitPackedTests() : UnitTest("BitPackedTests") {} void runTest() override { beginTest("Copy constructor"); { BitPacked p1; p1.add32Bit(0b10101010001010101010010101010010); p1.add32Bit(0b11110001001000101010101101001011); BitPacked p2(p1);
expectEquals(p2.count16Bit(), 4);
expectEquals(p2.get16Bit(0), (uint16_t)0b1010010101010010); expectEquals(p2.get16Bit(1), (uint16_t)0b1010101000101010); expectEquals(p2.get16Bit(2), (uint16_t)0b1010101101001011); expectEquals(p2.get16Bit(3), (uint16_t)0b1111000100100010); } }}static BitPackedTests bitPackedTests;
USEFUL TEST RUNNER TRICKS
> Ship your executable with all tests includedvoid LASFrontEndApplication::initialise(const String& commandLine){ // Fully setup and initialise the application // ...
// Run tests if necessary if (!testRunner_.scheduleTests()) // If no tests are going to run, populate with example data if that was requested // ... }}
LASTestRunner testRunner_;
USEFUL TEST RUNNER TRICKS
> Ship your executable with all tests includedbool LASTestRunner::Pimpl::scheduleTests() { bool runTests = false; #if JUCE_DEBUG if (!JUCEApplication::getInstance()->getCommandLineParameters().contains("--disable-tests")) { runTests = true; } #endif if (JUCEApplication::getInstance()->getCommandLineParameters().contains("--enable-tests")) { runTests = true; }
if (runTests) { (new ScheduleTestsCallback(this))->post(); }}
USEFUL TEST RUNNER TRICKS
> Run your tests asynchronouslystruct ScheduleTestsCallback : public CallbackMessage { ScheduleTestsCallback(LASTestRunner::Pimpl* runner) : runner_(runner) {} void messageCallback(); LASTestRunner::Pimpl* runner_;};
struct LASTestRunner::Pimpl : public Thread { Pimpl() : Thread("LASTestRunner thread") {} void run() { UnitTestRunner unitTestRunner; // ... }}
void ScheduleTestsCallback::messageCallback() { runner_->startThread();}
USEFUL TEST RUNNER TRICKS
> Easily focus on specific testsUnitTestRunner unitTestRunner;
Array<UnitTest*> focusedTests;Array<UnitTest*>& allTests = UnitTest::getAllTests();for (UnitTest* test : allTests) { if (test->getName().isEmpty { focusedTests.add(test); }}
if (focusedTests.isEmpty()) unitTestRunner.runTests(allTests);else unitTestRunner.runTests(focusedTests);
HOOK INTO CONTINUOUS INTEGRATION (GITLAB).test_template: &test_definition stage: test variables: GIT_STRATEGY: none image: gbevin/raspberry-ci script: - LAS_TEST=gitlab DISPLAY=:99 xvfb-run -n 99 \ -s "-ac -screen 0 800x480x16" Builds/LinuxMakefile/build/las-frontend --enable-tests artifacts: when: always paths: - las-frontend.log - las-frontend.db - las-screenshot*.png
test:pi2: <<: *test_definition tags: - raspberrypi2