From 4c49e56defa3bd141216773d32941dbd253bbc17 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 28 Oct 2026 21:17:49 +1000 Subject: [PATCH 1/5] up --- Apps/AirMouse/CMakeLists.txt | 13 + Apps/AirMouse/README.md | 167 +++ Apps/AirMouse/main/CMakeLists.txt | 4 + Apps/AirMouse/main/Source/AirMouse.cpp | 759 ++++++++++ Apps/AirMouse/main/Source/AirMouse.h | 135 ++ Apps/AirMouse/main/Source/MainView.cpp | 81 + Apps/AirMouse/main/Source/Motion.h | 521 +++++++ Apps/AirMouse/main/Source/SettingsView.cpp | 142 ++ Apps/AirMouse/main/Source/main.cpp | 67 + Apps/AirMouse/manifest.properties | 11 + Apps/Brainfuck/main/Source/Brainfuck.cpp | 28 +- Apps/Brainfuck/manifest.properties | 4 +- Apps/Breakout/README.md | 4 +- Apps/Breakout/main/Source/Breakout.cpp | 87 +- Apps/Breakout/manifest.properties | 4 +- Apps/Calculator/main/Source/Calculator.cpp | 4 +- Apps/Calculator/manifest.properties | 4 +- Apps/Diceware/main/Source/Diceware.cpp | 54 +- Apps/Diceware/manifest.properties | 4 +- Apps/Doom/main/Source/DoomAudio.cpp | 30 +- .../main/doomgeneric/doomgeneric/doomtype.h | 6 +- Apps/EpubReader/main/Source/EpubReader.cpp | 6 +- Apps/EpubReader/main/Source/EpubReaderUI.cpp | 14 +- Apps/EpubReader/manifest.properties | 7 +- .../EspNowBridge/main/Source/EspNowBridge.cpp | 6 +- Apps/EspNowBridge/manifest.properties | 4 +- Apps/GfxBenchmark/CMakeLists.txt | 13 + Apps/GfxBenchmark/README.md | 70 + Apps/GfxBenchmark/main/CMakeLists.txt | 11 + Apps/GfxBenchmark/main/Include/Application.h | 6 + Apps/GfxBenchmark/main/Source/Application.cpp | 847 +++++++++++ Apps/GfxBenchmark/main/Source/Main.cpp | 127 ++ Apps/GfxBenchmark/manifest.properties | 11 + Apps/GfxBenchmark/pics/phase-01.png | Bin 0 -> 2900 bytes Apps/GfxBenchmark/pics/phase-02.png | Bin 0 -> 2802 bytes Apps/GfxBenchmark/pics/phase-03.png | Bin 0 -> 5420 bytes Apps/GfxBenchmark/pics/phase-04.png | Bin 0 -> 1090 bytes Apps/GfxBenchmark/pics/phase-05.png | Bin 0 -> 1151 bytes Apps/GfxBenchmark/pics/phase-06.png | Bin 0 -> 1274 bytes Apps/GfxBenchmark/pics/phase-07.png | Bin 0 -> 1674 bytes Apps/GfxBenchmark/pics/phase-08.png | Bin 0 -> 1536 bytes Apps/GfxBenchmark/pics/phase-09.png | Bin 0 -> 6295 bytes Apps/GfxBenchmark/pics/phase-10.png | Bin 0 -> 4954 bytes Apps/GfxBenchmark/pics/phase-11.png | Bin 0 -> 3269 bytes Apps/GfxBenchmark/pics/phase-12.png | Bin 0 -> 7340 bytes Apps/GfxBenchmark/pics/phase-13.png | Bin 0 -> 1796 bytes Apps/GfxBenchmark/pics/phase-14.png | Bin 0 -> 3323 bytes Apps/GfxBenchmark/pics/phase-15.png | Bin 0 -> 3678 bytes Apps/GfxBenchmark/pics/phase-16.png | Bin 0 -> 1655 bytes Apps/GfxBenchmark/pics/results-320x240.png | Bin 0 -> 4274 bytes Apps/GraphicsDemo/main/CMakeLists.txt | 9 +- Apps/GraphicsDemo/main/Include/Application.h | 6 +- Apps/GraphicsDemo/main/Include/PixelBuffer.h | 125 -- .../main/Include/drivers/Colors.h | 35 - .../main/Include/drivers/DisplayDriver.h | 47 - .../main/Include/drivers/TouchDriver.h | 49 - Apps/GraphicsDemo/main/Source/Application.cpp | 147 +- Apps/GraphicsDemo/main/Source/Main.cpp | 66 +- Apps/M5UnitTest/main/Source/M5UnitTest.cpp | 7 +- Apps/M5UnitTest/main/Source/TestListView.cpp | 41 +- .../main/Source/TestUnit8Encoder.cpp | 34 +- .../M5UnitTest/main/Source/TestUnit8Encoder.h | 1 + .../main/Source/TestUnitByteButton.cpp | 46 +- .../main/Source/TestUnitByteButton.h | 1 + .../main/Source/TestUnitCardKB2.cpp | 71 +- Apps/M5UnitTest/main/Source/TestUnitCardKB2.h | 2 + .../main/Source/TestUnitJoystick2.cpp | 48 +- .../main/Source/TestUnitJoystick2.h | 1 + Apps/M5UnitTest/main/Source/TestUnitLcd.cpp | 25 +- Apps/M5UnitTest/main/Source/TestUnitLcd.h | 2 +- .../M5UnitTest/main/Source/TestUnitLcdGfx.cpp | 43 +- Apps/M5UnitTest/main/Source/TestUnitLcdGfx.h | 1 + Apps/M5UnitTest/main/Source/TestUnitPaHub.cpp | 127 +- Apps/M5UnitTest/main/Source/TestUnitPaHub.h | 1 - Apps/M5UnitTest/main/Source/TestUnitRfid2.cpp | 82 +- Apps/M5UnitTest/main/Source/TestUnitRfid2.h | 2 + .../M5UnitTest/main/Source/TestUnitScroll.cpp | 44 +- Apps/M5UnitTest/main/Source/TestUnitScroll.h | 1 + Apps/M5UnitTest/main/Source/TestViewBase.cpp | 6 +- Apps/M5UnitTest/main/Source/UiKit.h | 65 + Apps/M5UnitTest/main/Source/UiScale.h | 1 + Apps/M5UnitTest/manifest.properties | 4 +- Apps/Magic8Ball/main/Source/Magic8Ball.cpp | 75 +- Apps/Magic8Ball/main/Source/Magic8Ball.h | 1 + Apps/Magic8Ball/manifest.properties | 4 +- Apps/MediaKeys/README.md | 56 + Apps/MediaKeys/main/Source/MediaKeys.cpp | 58 +- Apps/MediaKeys/manifest.properties | 4 +- Apps/MidiController/CMakeLists.txt | 13 + Apps/MidiController/README.md | 52 + ...ntrol-Change-Messages_Another-Producer.pdf | Bin 0 -> 182253 bytes Apps/MidiController/docs/architecture.md | 161 ++ Apps/MidiController/docs/encoder.md | 158 ++ Apps/MidiController/docs/hardware.md | 133 ++ Apps/MidiController/docs/lcd.md | 162 ++ Apps/MidiController/docs/modes.md | 300 ++++ Apps/MidiController/docs/scales.md | 73 + Apps/MidiController/docs/units.md | 333 +++++ .../images/0_transport_selection.png | Bin 0 -> 17865 bytes Apps/MidiController/images/10_chord_mode.png | Bin 0 -> 36999 bytes Apps/MidiController/images/11_lfo_mode.png | Bin 0 -> 38735 bytes Apps/MidiController/images/12_raga_mode.png | Bin 0 -> 40573 bytes .../images/13_dimensions_mode.png | Bin 0 -> 41764 bytes Apps/MidiController/images/14_baby8_mode.png | Bin 0 -> 38557 bytes Apps/MidiController/images/15_tb3po_mode.png | Bin 0 -> 35347 bytes Apps/MidiController/images/16_euclid_mode.png | Bin 0 -> 71486 bytes .../images/1_mode_selection.png | Bin 0 -> 53507 bytes Apps/MidiController/images/2_keys_mode.png | Bin 0 -> 15096 bytes Apps/MidiController/images/3_beats_mode.png | Bin 0 -> 31370 bytes Apps/MidiController/images/4_zen_mode.png | Bin 0 -> 10928 bytes Apps/MidiController/images/5_drop_mode.png | Bin 0 -> 32245 bytes Apps/MidiController/images/6_rng_mode.png | Bin 0 -> 40620 bytes Apps/MidiController/images/7_xy_mode.png | Bin 0 -> 12226 bytes Apps/MidiController/images/8_arp_mode.png | Bin 0 -> 43456 bytes Apps/MidiController/images/9_grid_mode.png | Bin 0 -> 23991 bytes Apps/MidiController/main/CMakeLists.txt | 5 + .../main/Source/EncoderInput.cpp | 379 +++++ .../main/Source/GroveLookup.cpp | 45 + Apps/MidiController/main/Source/GroveLookup.h | 12 + .../MidiController/main/Source/LcdDisplay.cpp | 814 ++++++++++ Apps/MidiController/main/Source/LcdDisplay.h | 49 + .../main/Source/MidiController.cpp | 1054 +++++++++++++ .../main/Source/MidiController.h | 576 +++++++ Apps/MidiController/main/Source/MidiScales.h | 76 + Apps/MidiController/main/Source/Sequencer.cpp | 107 ++ Apps/MidiController/main/Source/Sequencer.h | 58 + Apps/MidiController/main/Source/UiKit.h | 159 ++ Apps/MidiController/main/Source/ViewArp.cpp | 541 +++++++ Apps/MidiController/main/Source/ViewBaby8.cpp | 417 ++++++ Apps/MidiController/main/Source/ViewBeats.cpp | 347 +++++ Apps/MidiController/main/Source/ViewChord.cpp | 298 ++++ .../main/Source/ViewDimensions.cpp | 575 +++++++ Apps/MidiController/main/Source/ViewDrop.cpp | 515 +++++++ .../MidiController/main/Source/ViewEuclid.cpp | 677 +++++++++ Apps/MidiController/main/Source/ViewGrid.cpp | 289 ++++ Apps/MidiController/main/Source/ViewKeys.cpp | 427 ++++++ Apps/MidiController/main/Source/ViewLfo.cpp | 449 ++++++ Apps/MidiController/main/Source/ViewRaga.cpp | 554 +++++++ Apps/MidiController/main/Source/ViewRng.cpp | 352 +++++ Apps/MidiController/main/Source/ViewTb3po.cpp | 636 ++++++++ Apps/MidiController/main/Source/ViewXYPad.cpp | 310 ++++ Apps/MidiController/main/Source/ViewZen.cpp | 521 +++++++ Apps/MidiController/main/Source/main.cpp | 64 + Apps/MidiController/manifest.properties | 12 + Apps/MystifyDemo/main/CMakeLists.txt | 9 +- Apps/MystifyDemo/main/Include/Application.h | 6 +- Apps/MystifyDemo/main/Include/MystifyDemo.h | 268 ++-- Apps/MystifyDemo/main/Include/PixelBuffer.h | 125 -- .../MystifyDemo/main/Include/drivers/Colors.h | 35 - .../main/Include/drivers/DisplayDriver.h | 47 - .../main/Include/drivers/TouchDriver.h | 49 - Apps/MystifyDemo/main/Source/Application.cpp | 104 +- Apps/MystifyDemo/main/Source/Main.cpp | 64 +- Apps/MystifySpiDemo/README.md | 170 +++ Apps/MystifySpiDemo/common/ili9341panel.h | 75 + Apps/MystifySpiDemo/common/panel_display.h | 84 ++ .../ili9341-panel-module/CMakeLists.txt | 13 + .../ili9341-panel-module/main/CMakeLists.txt | 12 + .../main/Source/Ili9341Spi.cpp | 667 +++++++++ .../main/Source/Ili9341Spi.h | 65 + .../main/Source/Panel.cpp | 148 ++ .../ili9341-panel-module/main/Source/Panel.h | 19 + .../main/Source/Settings.cpp | 186 +++ .../main/Source/Settings.h | 14 + .../main/Source/module.cpp | 112 ++ .../ili9341-panel-settings/CMakeLists.txt | 13 + .../main/CMakeLists.txt | 8 + .../main/Source/main.cpp | 1320 +++++++++++++++++ .../ili9341panel/CMakeLists.txt | 13 + .../ili9341panel/main/CMakeLists.txt | 8 + .../ili9341panel/main/Source/main.cpp | 475 ++++++ Apps/MystifySpiDemo/manifest.properties | 23 + .../mystifyspidemo/CMakeLists.txt | 13 + .../mystifyspidemo/main/CMakeLists.txt | 16 + .../mystifyspidemo/main/Include/Application.h | 14 + .../main/Source/Application.cpp | 362 +++++ .../mystifyspidemo/main/Source/Main.cpp | 98 ++ .../paneldashboard/CMakeLists.txt | 13 + .../paneldashboard/main/CMakeLists.txt | 8 + .../paneldashboard/main/Source/main.cpp | 406 +++++ .../SerialConsole/main/Source/ConnectView.cpp | 2 +- .../main/Source/SerialConsole.cpp | 8 +- Apps/SerialConsole/manifest.properties | 4 +- Apps/Snake/main/Source/Snake.cpp | 45 +- Apps/Snake/main/Source/SnakeUi.c | 10 +- Apps/Snake/manifest.properties | 4 +- Apps/TamaTac/README.md | 59 +- Apps/TamaTac/decor/scene_day.png | Bin 0 -> 3848 bytes Apps/TamaTac/decor/scene_night.png | Bin 0 -> 1655 bytes Apps/TamaTac/main/Source/Achievements.cpp | 16 +- Apps/TamaTac/main/Source/CemeteryView.cpp | 19 +- Apps/TamaTac/main/Source/Decor.h | 196 +++ Apps/TamaTac/main/Source/Layout.h | 124 ++ Apps/TamaTac/main/Source/MainView.cpp | 706 ++++++++- Apps/TamaTac/main/Source/MainView.h | 49 + Apps/TamaTac/main/Source/MenuView.cpp | 13 +- Apps/TamaTac/main/Source/PatternGame.cpp | 66 +- Apps/TamaTac/main/Source/PetStats.h | 2 +- Apps/TamaTac/main/Source/ReactionGame.cpp | 45 +- Apps/TamaTac/main/Source/ReactionGame.h | 3 +- Apps/TamaTac/main/Source/SettingsView.cpp | 96 +- Apps/TamaTac/main/Source/SettingsView.h | 2 +- Apps/TamaTac/main/Source/StatsView.cpp | 15 +- Apps/TamaTac/main/Source/TamaTac.cpp | 27 +- Apps/TamaTac/main/Source/main.cpp | 3 + Apps/TamaTac/manifest.properties | 7 +- Apps/TodoList/main/Source/TodoList.cpp | 31 +- Apps/TodoList/manifest.properties | 4 +- Apps/Tone/main/Source/ToneUi.cpp | 10 +- Apps/Tone/manifest.properties | 4 +- Apps/TwoEleven/main/Source/TwoEleven.cpp | 43 +- Apps/TwoEleven/main/Source/TwoElevenUi.c | 4 +- Apps/TwoEleven/manifest.properties | 4 +- Apps/UnitLcdGfxDemo/CMakeLists.txt | 13 + Apps/UnitLcdGfxDemo/README.md | 60 + Apps/UnitLcdGfxDemo/main/CMakeLists.txt | 20 + .../UnitLcdGfxDemo/main/Include/Application.h | 8 + .../UnitLcdGfxDemo/main/Include/GroveLookup.h | 8 + .../main/Source/Application.cpp | 454 ++++++ .../main/Source/GroveLookup.cpp | 41 + Apps/UnitLcdGfxDemo/main/Source/Main.cpp | 95 ++ Apps/UnitLcdGfxDemo/manifest.properties | 11 + Libraries/Gfx/DESIGN.md | 327 ++++ Libraries/Gfx/Include/gfx/Canvas.h | 61 + Libraries/Gfx/Include/gfx/DirtyRegions.h | 56 + Libraries/Gfx/Include/gfx/DisplaySink.h | 41 + Libraries/Gfx/Include/gfx/Font.h | 135 ++ Libraries/Gfx/Include/gfx/Gfx.h | 23 + Libraries/Gfx/Include/gfx/Rect.h | 58 + Libraries/Gfx/Include/gfx/Sink.h | 116 ++ Libraries/Gfx/Include/gfx/Surface.h | 266 ++++ Libraries/Gfx/Include/gfx/Target.h | 196 +++ Libraries/Gfx/Include/gfx/Text.h | 20 + Libraries/Gfx/LICENSE.md | 195 +++ Libraries/Gfx/Licenses/Terminus-OFL.txt | 99 ++ Libraries/Gfx/README.md | 329 ++++ Libraries/Gfx/Source/Canvas.cpp | 19 + Libraries/Gfx/Source/ColorUtils.h | 103 ++ Libraries/Gfx/Source/DisplaySink.cpp | 84 ++ Libraries/Gfx/Source/FontTerminus16.cpp | 254 ++++ Libraries/Gfx/Source/Fonts.cpp | 290 ++++ Libraries/Gfx/Source/Images.cpp | 332 +++++ Libraries/Gfx/Source/Polygon.cpp | 107 ++ Libraries/Gfx/Source/Shapes.cpp | 522 +++++++ Libraries/Gfx/Source/SinkTarget.cpp | 133 ++ Libraries/Gfx/Source/Surface.cpp | 345 +++++ Libraries/Gfx/Source/Target.cpp | 688 +++++++++ Libraries/Gfx/Source/Text.cpp | 446 ++++++ Libraries/Input/Include/input/AnyInput.h | 52 + Libraries/Input/LICENSE.md | 195 +++ Libraries/Input/Source/AnyInput.cpp | 139 ++ .../M5UnitModules/Include/Unit8Encoder.h | 4 +- Libraries/M5UnitModules/Include/UnitCardKB2.h | 6 + Libraries/M5UnitModules/Include/UnitCommon.h | 140 +- Libraries/M5UnitModules/Include/UnitPaHub.h | 7 +- Libraries/M5UnitModules/Include/UnitRfid2.h | 11 +- Libraries/M5UnitModules/README.md | 13 +- .../M5UnitModules/Source/Unit8Encoder.cpp | 59 +- .../M5UnitModules/Source/UnitCardKB2.cpp | 34 +- Libraries/M5UnitModules/Source/UnitLcd.cpp | 42 +- Libraries/M5UnitModules/Source/UnitPaHub.cpp | 84 +- Libraries/M5UnitModules/Source/UnitRfid2.cpp | 72 +- Libraries/M5UnitModules/UnitReferences.md | 70 +- Libraries/SfxEngine/Include/SfxDefinitions.h | 20 +- Libraries/SfxEngine/Include/SfxEngine.h | 43 +- Libraries/SfxEngine/README.md | 1 - Libraries/SfxEngine/Source/SfxEngine.cpp | 163 +- Libraries/UnitLcdGfx/Include/UnitLcdSink.h | 106 ++ Libraries/UnitLcdGfx/README.md | 53 + Scripts/check-app-symbols.py | 723 +++++++++ Scripts/sprite_tools/gen_bgdecor.py | 252 ++++ 271 files changed, 29676 insertions(+), 1543 deletions(-) create mode 100644 Apps/AirMouse/CMakeLists.txt create mode 100644 Apps/AirMouse/README.md create mode 100644 Apps/AirMouse/main/CMakeLists.txt create mode 100644 Apps/AirMouse/main/Source/AirMouse.cpp create mode 100644 Apps/AirMouse/main/Source/AirMouse.h create mode 100644 Apps/AirMouse/main/Source/MainView.cpp create mode 100644 Apps/AirMouse/main/Source/Motion.h create mode 100644 Apps/AirMouse/main/Source/SettingsView.cpp create mode 100644 Apps/AirMouse/main/Source/main.cpp create mode 100644 Apps/AirMouse/manifest.properties create mode 100644 Apps/GfxBenchmark/CMakeLists.txt create mode 100644 Apps/GfxBenchmark/README.md create mode 100644 Apps/GfxBenchmark/main/CMakeLists.txt create mode 100644 Apps/GfxBenchmark/main/Include/Application.h create mode 100644 Apps/GfxBenchmark/main/Source/Application.cpp create mode 100644 Apps/GfxBenchmark/main/Source/Main.cpp create mode 100644 Apps/GfxBenchmark/manifest.properties create mode 100644 Apps/GfxBenchmark/pics/phase-01.png create mode 100644 Apps/GfxBenchmark/pics/phase-02.png create mode 100644 Apps/GfxBenchmark/pics/phase-03.png create mode 100644 Apps/GfxBenchmark/pics/phase-04.png create mode 100644 Apps/GfxBenchmark/pics/phase-05.png create mode 100644 Apps/GfxBenchmark/pics/phase-06.png create mode 100644 Apps/GfxBenchmark/pics/phase-07.png create mode 100644 Apps/GfxBenchmark/pics/phase-08.png create mode 100644 Apps/GfxBenchmark/pics/phase-09.png create mode 100644 Apps/GfxBenchmark/pics/phase-10.png create mode 100644 Apps/GfxBenchmark/pics/phase-11.png create mode 100644 Apps/GfxBenchmark/pics/phase-12.png create mode 100644 Apps/GfxBenchmark/pics/phase-13.png create mode 100644 Apps/GfxBenchmark/pics/phase-14.png create mode 100644 Apps/GfxBenchmark/pics/phase-15.png create mode 100644 Apps/GfxBenchmark/pics/phase-16.png create mode 100644 Apps/GfxBenchmark/pics/results-320x240.png delete mode 100644 Apps/GraphicsDemo/main/Include/PixelBuffer.h delete mode 100644 Apps/GraphicsDemo/main/Include/drivers/Colors.h delete mode 100644 Apps/GraphicsDemo/main/Include/drivers/DisplayDriver.h delete mode 100644 Apps/GraphicsDemo/main/Include/drivers/TouchDriver.h create mode 100644 Apps/M5UnitTest/main/Source/UiKit.h create mode 100644 Apps/MediaKeys/README.md create mode 100644 Apps/MidiController/CMakeLists.txt create mode 100644 Apps/MidiController/README.md create mode 100644 Apps/MidiController/docs/MIDI-Control-Change-Messages_Another-Producer.pdf create mode 100644 Apps/MidiController/docs/architecture.md create mode 100644 Apps/MidiController/docs/encoder.md create mode 100644 Apps/MidiController/docs/hardware.md create mode 100644 Apps/MidiController/docs/lcd.md create mode 100644 Apps/MidiController/docs/modes.md create mode 100644 Apps/MidiController/docs/scales.md create mode 100644 Apps/MidiController/docs/units.md create mode 100644 Apps/MidiController/images/0_transport_selection.png create mode 100644 Apps/MidiController/images/10_chord_mode.png create mode 100644 Apps/MidiController/images/11_lfo_mode.png create mode 100644 Apps/MidiController/images/12_raga_mode.png create mode 100644 Apps/MidiController/images/13_dimensions_mode.png create mode 100644 Apps/MidiController/images/14_baby8_mode.png create mode 100644 Apps/MidiController/images/15_tb3po_mode.png create mode 100644 Apps/MidiController/images/16_euclid_mode.png create mode 100644 Apps/MidiController/images/1_mode_selection.png create mode 100644 Apps/MidiController/images/2_keys_mode.png create mode 100644 Apps/MidiController/images/3_beats_mode.png create mode 100644 Apps/MidiController/images/4_zen_mode.png create mode 100644 Apps/MidiController/images/5_drop_mode.png create mode 100644 Apps/MidiController/images/6_rng_mode.png create mode 100644 Apps/MidiController/images/7_xy_mode.png create mode 100644 Apps/MidiController/images/8_arp_mode.png create mode 100644 Apps/MidiController/images/9_grid_mode.png create mode 100644 Apps/MidiController/main/CMakeLists.txt create mode 100644 Apps/MidiController/main/Source/EncoderInput.cpp create mode 100644 Apps/MidiController/main/Source/GroveLookup.cpp create mode 100644 Apps/MidiController/main/Source/GroveLookup.h create mode 100644 Apps/MidiController/main/Source/LcdDisplay.cpp create mode 100644 Apps/MidiController/main/Source/LcdDisplay.h create mode 100644 Apps/MidiController/main/Source/MidiController.cpp create mode 100644 Apps/MidiController/main/Source/MidiController.h create mode 100644 Apps/MidiController/main/Source/MidiScales.h create mode 100644 Apps/MidiController/main/Source/Sequencer.cpp create mode 100644 Apps/MidiController/main/Source/Sequencer.h create mode 100644 Apps/MidiController/main/Source/UiKit.h create mode 100644 Apps/MidiController/main/Source/ViewArp.cpp create mode 100644 Apps/MidiController/main/Source/ViewBaby8.cpp create mode 100644 Apps/MidiController/main/Source/ViewBeats.cpp create mode 100644 Apps/MidiController/main/Source/ViewChord.cpp create mode 100644 Apps/MidiController/main/Source/ViewDimensions.cpp create mode 100644 Apps/MidiController/main/Source/ViewDrop.cpp create mode 100644 Apps/MidiController/main/Source/ViewEuclid.cpp create mode 100644 Apps/MidiController/main/Source/ViewGrid.cpp create mode 100644 Apps/MidiController/main/Source/ViewKeys.cpp create mode 100644 Apps/MidiController/main/Source/ViewLfo.cpp create mode 100644 Apps/MidiController/main/Source/ViewRaga.cpp create mode 100644 Apps/MidiController/main/Source/ViewRng.cpp create mode 100644 Apps/MidiController/main/Source/ViewTb3po.cpp create mode 100644 Apps/MidiController/main/Source/ViewXYPad.cpp create mode 100644 Apps/MidiController/main/Source/ViewZen.cpp create mode 100644 Apps/MidiController/main/Source/main.cpp create mode 100644 Apps/MidiController/manifest.properties delete mode 100644 Apps/MystifyDemo/main/Include/PixelBuffer.h delete mode 100644 Apps/MystifyDemo/main/Include/drivers/Colors.h delete mode 100644 Apps/MystifyDemo/main/Include/drivers/DisplayDriver.h delete mode 100644 Apps/MystifyDemo/main/Include/drivers/TouchDriver.h create mode 100644 Apps/MystifySpiDemo/README.md create mode 100644 Apps/MystifySpiDemo/common/ili9341panel.h create mode 100644 Apps/MystifySpiDemo/common/panel_display.h create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.cpp create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.h create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.cpp create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.h create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Settings.cpp create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Settings.h create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/Source/module.cpp create mode 100644 Apps/MystifySpiDemo/ili9341-panel-settings/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/ili9341-panel-settings/main/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/ili9341-panel-settings/main/Source/main.cpp create mode 100644 Apps/MystifySpiDemo/ili9341panel/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/ili9341panel/main/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/ili9341panel/main/Source/main.cpp create mode 100644 Apps/MystifySpiDemo/manifest.properties create mode 100644 Apps/MystifySpiDemo/mystifyspidemo/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/mystifyspidemo/main/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/mystifyspidemo/main/Include/Application.h create mode 100644 Apps/MystifySpiDemo/mystifyspidemo/main/Source/Application.cpp create mode 100644 Apps/MystifySpiDemo/mystifyspidemo/main/Source/Main.cpp create mode 100644 Apps/MystifySpiDemo/paneldashboard/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/paneldashboard/main/CMakeLists.txt create mode 100644 Apps/MystifySpiDemo/paneldashboard/main/Source/main.cpp create mode 100644 Apps/TamaTac/decor/scene_day.png create mode 100644 Apps/TamaTac/decor/scene_night.png create mode 100644 Apps/TamaTac/main/Source/Decor.h create mode 100644 Apps/TamaTac/main/Source/Layout.h create mode 100644 Apps/UnitLcdGfxDemo/CMakeLists.txt create mode 100644 Apps/UnitLcdGfxDemo/README.md create mode 100644 Apps/UnitLcdGfxDemo/main/CMakeLists.txt create mode 100644 Apps/UnitLcdGfxDemo/main/Include/Application.h create mode 100644 Apps/UnitLcdGfxDemo/main/Include/GroveLookup.h create mode 100644 Apps/UnitLcdGfxDemo/main/Source/Application.cpp create mode 100644 Apps/UnitLcdGfxDemo/main/Source/GroveLookup.cpp create mode 100644 Apps/UnitLcdGfxDemo/main/Source/Main.cpp create mode 100644 Apps/UnitLcdGfxDemo/manifest.properties create mode 100644 Libraries/Gfx/DESIGN.md create mode 100644 Libraries/Gfx/Include/gfx/Canvas.h create mode 100644 Libraries/Gfx/Include/gfx/DirtyRegions.h create mode 100644 Libraries/Gfx/Include/gfx/DisplaySink.h create mode 100644 Libraries/Gfx/Include/gfx/Font.h create mode 100644 Libraries/Gfx/Include/gfx/Gfx.h create mode 100644 Libraries/Gfx/Include/gfx/Rect.h create mode 100644 Libraries/Gfx/Include/gfx/Sink.h create mode 100644 Libraries/Gfx/Include/gfx/Surface.h create mode 100644 Libraries/Gfx/Include/gfx/Target.h create mode 100644 Libraries/Gfx/Include/gfx/Text.h create mode 100644 Libraries/Gfx/LICENSE.md create mode 100644 Libraries/Gfx/Licenses/Terminus-OFL.txt create mode 100644 Libraries/Gfx/README.md create mode 100644 Libraries/Gfx/Source/Canvas.cpp create mode 100644 Libraries/Gfx/Source/ColorUtils.h create mode 100644 Libraries/Gfx/Source/DisplaySink.cpp create mode 100644 Libraries/Gfx/Source/FontTerminus16.cpp create mode 100644 Libraries/Gfx/Source/Fonts.cpp create mode 100644 Libraries/Gfx/Source/Images.cpp create mode 100644 Libraries/Gfx/Source/Polygon.cpp create mode 100644 Libraries/Gfx/Source/Shapes.cpp create mode 100644 Libraries/Gfx/Source/SinkTarget.cpp create mode 100644 Libraries/Gfx/Source/Surface.cpp create mode 100644 Libraries/Gfx/Source/Target.cpp create mode 100644 Libraries/Gfx/Source/Text.cpp create mode 100644 Libraries/Input/Include/input/AnyInput.h create mode 100644 Libraries/Input/LICENSE.md create mode 100644 Libraries/Input/Source/AnyInput.cpp create mode 100644 Libraries/UnitLcdGfx/Include/UnitLcdSink.h create mode 100644 Libraries/UnitLcdGfx/README.md create mode 100644 Scripts/check-app-symbols.py create mode 100644 Scripts/sprite_tools/gen_bgdecor.py diff --git a/Apps/AirMouse/CMakeLists.txt b/Apps/AirMouse/CMakeLists.txt new file mode 100644 index 0000000..a84689d --- /dev/null +++ b/Apps/AirMouse/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(airmouse) +project(airmouse) +tactility_project_post(airmouse) diff --git a/Apps/AirMouse/README.md b/Apps/AirMouse/README.md new file mode 100644 index 0000000..b07e85e --- /dev/null +++ b/Apps/AirMouse/README.md @@ -0,0 +1,167 @@ +# Air Mouse + +Turns the device into a Bluetooth LE mouse: rotate the device to move the pointer and tap the two +on-screen pads for the left and right buttons. The device pairs with a PC, phone or tablet like any +other Bluetooth mouse. + +The app reads the gyroscope (not the accelerometer tilt), so the pointer follows rotation: turn the +device left and the pointer goes left, regardless of how you hold it. Gravity is used to work out how +the device is held, so it does not matter whether it is held upright or on its side. + +## Requirements + +- **Gyroscope.** Either an IMU in the devicetree (Tactility's drivers apply their own mount matrix), + or a bare MPU6886 module on an exposed I2C bus, which the app finds at address 0x68. Without one + the screen says "No IMU found". +- **Bluetooth.** The Bluetooth device has to be enabled in the devicetree. The radio itself may be + off: switching the pointer on turns it on, and the app turns it back off when it exits if it was + off before. If the radio is already on when the app opens, the pointer switches on by itself. +- **The window has to be visible.** The pointer only moves while the Air Mouse window is the one on + screen. Put it on a device you hold in your hand, or on a device whose window stays on top. + +## Pairing + +The app advertises as `Tactility Air Mouse` with its own Bluetooth address and bond, so a host pairs +it as a separate device from the other Tactility Bluetooth apps (Media Keys, MIDI). Pair it once per +host. Switching apps does not need a new pairing, the host reconnects each device on sight. + +## Main screen + +The toolbar has the title, a switch and the settings button. + +- **Switch:** turns the Bluetooth mouse on and off. On means: Bluetooth radio on, HID mouse started, + advertising, and the pointer following the gyroscope. Switching it off stops the HID mouse and puts + the radio back the way it was. Opening the app while the radio is already on switches the pointer + on. +- **Left / Right pads:** pressing a pad holds that mouse button down, releasing it lets go. The pads + are large so they can be used without looking. They do nothing when no host is connected. +- When the Bluetooth radio is turned off elsewhere (Tactility's own settings, or the host going + away), the switch turns itself off. + +### Clutch + +Hold **both pads at once** to freeze the pointer, then move the device to a more comfortable position +and let go. The pointer picks up where it was - no flinging the cursor across the screen while you +reposition your arm. + +That needs a touch screen, so devices without one get the **auto-clutch** instead (see below). + +### Auto-clutch + +While the auto-clutch is on, the app watches for the acceleration of the device being picked up or +carried - which is much stronger than the acceleration of steering - and freezes the pointer for as +long as that lasts, plus a quarter of a second. The status line in the settings screen says +"Repositioning - pointer frozen" while it is holding. + +It is on by default on devices without a touch screen (where the manual clutch cannot be pressed at +all) and off by default on touch devices. The switch in the settings screen overrides the default. + +## Settings screen + +Reached with the settings button in the toolbar; the arrow in the toolbar goes back. Changes are +applied immediately and saved when you leave the screen or close the app. + +- **Status line:** the current gyro bias in degrees per second and whether the axis mapping is + following gravity (`tilt-aware`) or using the fixed one, or a progress text while calibrating. +- **Sensitivity (%):** 25 to 300 in steps of 5, default 100. Scales the pointer speed. +- **Pointer direction:** cycles through `Inv:off`, `Inv:X`, `Inv:Y` and `Inv:XY` - for when the + pointer moves the wrong way (a device held upside down, or a swapped axis). +- **Tilt-aware orientation:** on by default. On, gravity decides which way is up, so the same + movement moves the pointer the same way whether the device is held upright, on its left side or on + its right side. Off, the axes are fixed to the device. Tactility's auto-rotate setting does not + affect it. +- **Freeze pointer while repositioning:** the auto-clutch described above. +- **Calibrate gyro:** measures the gyro bias again. Keep the device still. + +## Calibration + +A gyro reads a small non-zero rate when it is not moving, and that offset drifts with temperature. A +bias measured while the device sits still is subtracted from every sample, and: + +- The bias is measured automatically every time the pointer is switched on. Keep the device still for + a moment; the settings screen shows "Keep still: calibrating..." until enough samples have been + collected. The estimate is the median of a short window (~1.5 seconds), so the moments where your + hand moved are ignored; if the device keeps being moved the window is started again a few times + and after ~4.5 seconds the best estimate so far is used. +- The median is deliberate: it does not assume the sensor's own bias is small. Breakout IMU modules + (a bare MPU6886, for example) commonly sit at several degrees per second, and the datasheet allows + up to ±20 °/s, while the sensors built into the board are usually well under 1 °/s. Any scheme that + discards "samples above X °/s" to reject movement fails completely on a module whose *bias* is + above X, because then every sample is discarded. +- While the device is still, the bias is slowly tracked, so it does not creep over a long session - + including the slow drift while the module warms up. +- The **Calibrate gyro** button starts a new measurement at any time. + +Calibration only fixes the bias. If the pointer still drifts after a calibration, the bias readout on +the settings screen tells you which of the two it is: a reading near 0 while the pointer creeps means +the measurement itself is being lost, while a reading that keeps changing as the module warms up is +thermal drift (the tracking above follows it, and a fresh calibration pins it again). + +## Filtering and pointer movement + +The pointer is not the raw gyro reading: + +- A One Euro filter removes jitter when the device is nearly still while keeping the response quick + when it is moved. +- A small deadzone ignores the movement of a hand holding the device still. +- Movement is scaled by the time since the last sample, so the speed is the same at any update rate. +- Fractions of a pixel are carried over to the next sample, so slow movements still move the pointer + instead of being rounded away, and a single sample cannot jump the pointer more than 40 pixels. + +Horizontal movement is scaled more than vertical movement, because turning the device sideways covers +more angle than tilting it up or down. + +## Stored data + +The app keeps one file in its user data folder (`user/app/tactility.airmouse`): + +- `settings.properties`: `sens` (25-300), `inv` (0-3: X, Y or both inverted), `orient` (tilt-aware + mapping), `autoclutch` and `mm0`..`mm8` (the mount matrix below). The file is written when you + leave the settings screen or close the app. + +### Mount matrix + +A bare MPU6886 module has no devicetree entry, so nothing tells the app how it is mounted. `mm0` to +`mm8` are a row-major 3x3 matrix in the usual Linux IIO `mount-matrix` convention: + +``` +x' = m0*x + m1*y + m2*z +y' = m3*x + m4*y + m5*z +z' = m6*x + m7*y + m8*z +``` + +It maps the module's axes onto the device frame (+X right, +Y to the top of the screen, +Z out of the +screen toward the user). The module frame is right-handed, so two of its axes fix the third +(Z = X x Y). For a module lying flat against the device, the four possibilities are: + +| Module X | Module Y | Module Z | mm0..mm8 | +| --- | --- | --- | --- | +| right | up (to the top of the screen) | out of the screen | `1 0 0 / 0 1 0 / 0 0 1` | +| left | up | into the screen | `-1 0 0 / 0 1 0 / 0 0 -1` | +| right | down | into the screen | `1 0 0 / 0 -1 0 / 0 0 -1` | +| left | down | out of the screen | `-1 0 0 / 0 -1 0 / 0 0 1` | + +The built-in default (`kDefaultExtMountMatrix` in `main/Source/AirMouse.cpp`) is the second row: the +module flat on the back (or bottom, on a device whose screen faces up, in which case the sticker ends +up facing the floor, so the module's Z goes through the device, away from the screen) with its Y +towards the top of the screen and its X to the device's left. A module mounted in one of the other +orientations needs either that constant changed or the `mm0`..`mm8` keys set. + +`mm0` to `mm8` in the settings file win over the default, so a device that already has a settings file +keeps the matrix it saved. Remove the `mm*` keys (or the file) to fall back to the default. Edit the +file while the app is closed - it is read once, when the app starts. + +If the axes are right but the movement is mirrored, use **Pointer direction** in the settings instead; +the matrix only has to get the three axes into the right place. + +## Limitations + +- One mouse, no keyboard, no scrolling wheel. +- The pointer only moves while the app's window is on screen. +- The auto-clutch is a threshold on how hard the device is being moved: brisk steering can freeze the + pointer, and very gentle repositioning can move it. Turn it off if that gets in the way. +- Closing the app stops the HID mouse, so the host shows the device as disconnected until the app is + opened again. +- The app cannot start while another Bluetooth device is connected to Tactility in host mode (for + example a paired BLE keyboard). Disconnect that device first. +- Devices without a gyroscope cannot use the app; devices without Bluetooth can open it but not pair. diff --git a/Apps/AirMouse/main/CMakeLists.txt b/Apps/AirMouse/main/CMakeLists.txt new file mode 100644 index 0000000..ff3fb52 --- /dev/null +++ b/Apps/AirMouse/main/CMakeLists.txt @@ -0,0 +1,4 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +tactility_component_register(SRCS ${SOURCE_FILES}) diff --git a/Apps/AirMouse/main/Source/AirMouse.cpp b/Apps/AirMouse/main/Source/AirMouse.cpp new file mode 100644 index 0000000..931a903 --- /dev/null +++ b/Apps/AirMouse/main/Source/AirMouse.cpp @@ -0,0 +1,759 @@ +#include "AirMouse.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +const char* TAG = "AirMouse"; +const char* DEVICE_NAME = "Tactility Air Mouse"; + +/** Must match manifest.properties' app.id */ +constexpr const char* APP_ID = "tactility.airmouse"; +constexpr const char* SETTINGS_FILE = "settings.properties"; +// Motion settings keys +constexpr const char* PREF_SENSITIVITY = "sens"; +constexpr const char* PREF_INVERT = "inv"; +constexpr const char* PREF_ORIENTATION = "orient"; +constexpr const char* PREF_AUTOCLUTCH = "autoclutch"; +constexpr const char* PREF_MOUNT = "mm"; // mm0..mm8 + +// ── IMU discovery (device tree, or a bare external MPU6886 on any I2C bus) ───── + +constexpr TickType_t kExtTimeout = pdMS_TO_TICKS(10); +constexpr float kExtAccelScale = 8.0f / 32768.0f; // g per LSB (±8g) +constexpr float kExtGyroScale = 2000.0f / 32768.0f; // °/s per LSB (±2000°/s) + +// Default orientation of a bare external MPU6886: a module has no devicetree entry, so there is +// nothing else to tell us how it is mounted. This maps the module's axes onto the device frame +// (+X right, +Y to the top of the screen, +Z out of the screen toward the user). +// +// It assumes the module lies flat against the back of the device - on a device whose screen faces +// up, that is the bottom, so the sticker ends up facing the floor - which puts the module's Z +// through the device, away from the screen (a right-handed frame fixes it: Z = X x Y, and with X to +// the device's left and Y towards the top of the screen, Z can only point that way). Read from +// below, the sticker then shows its Y towards the top of the screen and its X to the device's left, +// which from the front reads as X to the left. A 180 degree turn about Y. +// +// Change this constant if the module is mounted differently, or per device by setting mm0..mm8: +// the settings file overrides this default whenever it has the keys. +constexpr airmouse::MountMatrix kDefaultExtMountMatrix = {{ -1, 0, 0, 0, 1, 0, 0, 0, -1 }}; + +int16_t extToI16(uint8_t hi, uint8_t lo) { + return static_cast(static_cast(hi) << 8 | lo); +} + +void initExtMpu6886(Device* i2c, uint8_t addr) { + // Wake from sleep (clear all PWR_MGMT_1 bits) + if (i2c_controller_register8_set(i2c, addr, 0x6B, 0x00, kExtTimeout) != ERROR_NONE) return; + vTaskDelay(pdMS_TO_TICKS(10)); + + // Device reset (bit 7) + if (i2c_controller_register8_set(i2c, addr, 0x6B, 0x80, kExtTimeout) != ERROR_NONE) return; + vTaskDelay(pdMS_TO_TICKS(10)); + + // Select auto clock (CLKSEL=1: PLL with gyro reference when available) + if (i2c_controller_register8_set(i2c, addr, 0x6B, 0x01, kExtTimeout) != ERROR_NONE) return; + vTaskDelay(pdMS_TO_TICKS(10)); + + // Configure accel: ±8g + if (i2c_controller_register8_set(i2c, addr, 0x1C, 0x10, kExtTimeout) != ERROR_NONE) return; + // Configure gyro: ±2000°/s + if (i2c_controller_register8_set(i2c, addr, 0x1B, 0x18, kExtTimeout) != ERROR_NONE) return; + // DLPF: gyro BW=176Hz + if (i2c_controller_register8_set(i2c, addr, 0x1A, 0x01, kExtTimeout) != ERROR_NONE) return; + // Sample rate: 1kHz / (1+5) = 166Hz + if (i2c_controller_register8_set(i2c, addr, 0x19, 0x05, kExtTimeout) != ERROR_NONE) return; + // Clear interrupt enables before reconfiguring + if (i2c_controller_register8_set(i2c, addr, 0x38, 0x00, kExtTimeout) != ERROR_NONE) return; + // Accel low-pass filter: ACCEL_FCHOICE_B=0, A_DLPF_CFG=0 + if (i2c_controller_register8_set(i2c, addr, 0x1D, 0x00, kExtTimeout) != ERROR_NONE) return; + // Disable DMP and FIFO + if (i2c_controller_register8_set(i2c, addr, 0x6A, 0x00, kExtTimeout) != ERROR_NONE) return; + if (i2c_controller_register8_set(i2c, addr, 0x23, 0x00, kExtTimeout) != ERROR_NONE) return; + // Interrupt: active-high (ACTL=0), push-pull (OPEN=0), latched (LATCH_INT_EN=1), cleared on any read (INT_ANYRD_2CLEAR=1) → 0x30 + if (i2c_controller_register8_set(i2c, addr, 0x37, 0x30, kExtTimeout) != ERROR_NONE) return; + // Enable DATA_RDY interrupt + if (i2c_controller_register8_set(i2c, addr, 0x38, 0x01, kExtTimeout) != ERROR_NONE) return; + vTaskDelay(pdMS_TO_TICKS(100)); +} + +error_t readExtMpu6886Gyro(Device* i2c, uint8_t addr, const airmouse::MountMatrix& matrix, ImuGyroData* out) { + uint8_t buf[6] = {}; + error_t err = i2c_controller_read_register(i2c, addr, 0x43, buf, sizeof(buf), kExtTimeout); + if (err != ERROR_NONE) return err; + float gx = extToI16(buf[0], buf[1]) * kExtGyroScale; + float gy = extToI16(buf[2], buf[3]) * kExtGyroScale; + float gz = extToI16(buf[4], buf[5]) * kExtGyroScale; + matrix.apply(gx, gy, gz); + out->gx = gx; + out->gy = gy; + out->gz = gz; + return ERROR_NONE; +} + +error_t readExtMpu6886Accel(Device* i2c, uint8_t addr, const airmouse::MountMatrix& matrix, ImuAccelData* out) { + uint8_t buf[6] = {}; + error_t err = i2c_controller_read_register(i2c, addr, 0x3B, buf, sizeof(buf), kExtTimeout); + if (err != ERROR_NONE) return err; + float ax = extToI16(buf[0], buf[1]) * kExtAccelScale; + float ay = extToI16(buf[2], buf[3]) * kExtAccelScale; + float az = extToI16(buf[4], buf[5]) * kExtAccelScale; + matrix.apply(ax, ay, az); + out->ax = ax; + out->ay = ay; + out->az = az; + return ERROR_NONE; +} + +// Reads accel + gyro in the device frame. For a devicetree IMU the driver has +// already applied its mount-matrix; for the external module we apply the one +// stored in Context (identity by default). +// +// Gyro is mandatory; accel only feeds orientation mapping, so if a driver has no +// accel-only read path we zero it and the pipeline falls back to the fixed +// mapping rather than failing. +error_t ctxReadMotion(Context* ctx, ImuAccelData* accel, ImuGyroData* gyro) { + *accel = ImuAccelData {}; + if (ctx->extI2cCtrl) { + error_t err = readExtMpu6886Gyro(ctx->extI2cCtrl, ctx->extImuAddr, ctx->extMountMatrix, gyro); + if (err != ERROR_NONE) return err; + readExtMpu6886Accel(ctx->extI2cCtrl, ctx->extImuAddr, ctx->extMountMatrix, accel); + return ERROR_NONE; + } + if (ctx->imuDevice) { + error_t err = imu_read_gyro(ctx->imuDevice, gyro); + if (err != ERROR_NONE) return err; + imu_read_accel(ctx->imuDevice, accel); + return ERROR_NONE; + } + return ERROR_NOT_FOUND; +} + +void discoverImu(Context* ctx) { + Device* found = nullptr; + if (device_get_first_by_type(&IMU_TYPE, &found) == ERROR_NONE) { + ctx->imuDevice = found; + return; + } + + // No device-tree IMU - scan I2C buses for a bare external MPU6886 module. Retried a few + // times: a module can still be settling on the bus (or the bus itself still coming up) right + // at app-launch time, when this runs - the real mpu6886 driver hits the same thing at power-up + // and retries its own WHO_AM_I read for the same reason (see mpu6886.cpp's start()). + struct ScanCtx { Device* ctrl; uint8_t addr; }; + ScanCtx scanCtx = { nullptr, 0x68 }; + static constexpr int kScanAttempts = 5; + for (int attempt = 0; attempt < kScanAttempts && !scanCtx.ctrl; attempt++) { + if (attempt > 0) vTaskDelay(pdMS_TO_TICKS(20)); + device_for_each_of_type(&I2C_CONTROLLER_TYPE, &scanCtx, [](Device* dev, void* c) -> bool { + auto* sc = static_cast(c); + if (!device_is_ready(dev)) return true; + if (i2c_controller_has_device_at_address(dev, sc->addr, pdMS_TO_TICKS(10)) == ERROR_NONE) { + uint8_t whoami = 0; + if (i2c_controller_read_register(dev, sc->addr, 0x75, &whoami, 1, pdMS_TO_TICKS(10)) == ERROR_NONE && whoami == 0x19) { + sc->ctrl = dev; + return false; + } + } + return true; + }); + } + if (scanCtx.ctrl) { + ctx->extI2cCtrl = scanCtx.ctrl; + ctx->extImuAddr = scanCtx.addr; + initExtMpu6886(ctx->extI2cCtrl, ctx->extImuAddr); + LOG_I(TAG, "External MPU6886 found at 0x%02X", ctx->extImuAddr); + } +} + +// ── Settings (persisted) ────────────────────────────────────────────────────── + +bool getSettingsPath(char* buf, size_t size) { + return app_paths_get_user_data_path(APP_ID, SETTINGS_FILE, buf, size) == ERROR_NONE; +} + +// True when the device has a pointer/touch input device. Without one, the user +// can't press the on-screen click pads at all - let alone both at once for the +// manual clutch - so the auto-clutch is the only way to freeze the pointer, and +// we default it on for those devices (its own settings toggle would likely be +// just as unreachable). +bool hasTouchInput() { + // The LVGL indev list is static after boot in practice, but this runs from the + // app's own task (via loadSettings), not the LVGL task, so take the lock. + const bool locked = lvgl_try_lock(1000); + bool found = false; + for (lv_indev_t* indev = lv_indev_get_next(nullptr); indev != nullptr; indev = lv_indev_get_next(indev)) { + if (lv_indev_get_type(indev) == LV_INDEV_TYPE_POINTER) { found = true; break; } + } + if (locked) lvgl_unlock(); + return found; +} + +void loadSettings(Context* ctx) { + ctx->sensitivityPercent = 100; + ctx->invertMode = 0; + ctx->useOrientation = true; + ctx->autoClutch = !hasTouchInput(); // no touch screen = can't use the manual clutch + ctx->extMountMatrix = kDefaultExtMountMatrix; + + char path[192]; + if (getSettingsPath(path, sizeof(path))) { + Preferences* prefs = preferences_open(path); + if (prefs) { + int32_t value = 0; + if (preferences_opt_int32(prefs, PREF_SENSITIVITY, &value)) ctx->sensitivityPercent = value; + if (preferences_opt_int32(prefs, PREF_INVERT, &value)) ctx->invertMode = value; + bool enabled = true; + if (preferences_opt_bool(prefs, PREF_ORIENTATION, &enabled)) ctx->useOrientation = enabled; + if (preferences_opt_bool(prefs, PREF_AUTOCLUTCH, &enabled)) ctx->autoClutch = enabled; + for (int i = 0; i < 9; i++) { + char key[8]; + snprintf(key, sizeof(key), "%s%d", PREF_MOUNT, i); + if (preferences_opt_int32(prefs, key, &value)) ctx->extMountMatrix.m[i] = value; + } + preferences_close(prefs); + } + } + + if (ctx->sensitivityPercent < 25) ctx->sensitivityPercent = 25; + if (ctx->sensitivityPercent > 300) ctx->sensitivityPercent = 300; + ctx->invertMode &= 0x03; + applySettings(ctx); +} + +// ── Motion processing ───────────────────────────────────────────────────────── + +void onMotionTimer(lv_timer_t* t) { + auto* ctx = static_cast(lv_timer_get_user_data(t)); + if (!ctx) return; + // Widgets only exist while this window is topmost - skip otherwise (buried by another app + // started non-modally; window_manager deletes a buried window's widgets, so touching them + // here would use-after-free them). The timer itself isn't destroyed by burial, only by + // airMouseTeardown(), so it can still fire while buried. + if (window_manager_get_state(ctx->window) != WINDOW_STATE_GRANTED) return; + + ImuAccelData accel {}; + ImuGyroData gyro {}; + if (ctxReadMotion(ctx, &accel, &gyro) != ERROR_NONE) return; + + if (ctx->gyroLabel) { + char buf[48]; + if (ctx->motion.isCalibrating()) { + lv_label_set_text(ctx->gyroLabel, "Keep still: calibrating..."); + } else if (ctx->motion.isAutoClutching()) { + lv_label_set_text(ctx->gyroLabel, "Repositioning - pointer frozen"); + } else { + snprintf(buf, sizeof(buf), "bias:%.2f %s", ctx->motion.biasMagnitude(), + ctx->motion.orientationActive() ? "tilt-aware" : "fixed"); + lv_label_set_text(ctx->gyroLabel, buf); + } + } + + uint32_t now = lv_tick_get(); + float dt = (ctx->lastMotionTick == 0) + ? (1.0f / 33.0f) + : static_cast(now - ctx->lastMotionTick) / 1000.0f; + ctx->lastMotionTick = now; + if (dt > 0.2f) dt = 0.2f; // long stall (app was buried): don't lurch on resume + + // Always run the pipeline while the app is on screen - even with the pointer off or the + // clutch held - so gyro calibration, the bias drift tracker and the gravity estimate stay + // current. Only the *sending* is gated below; the pipeline drains its own sub-pixel carry + // every tick, so discarding the output can't leave pent-up motion to "catch up" later. + int dx = 0; + int dy = 0; + bool moved = ctx->motion.update(accel.ax, accel.ay, accel.az, + gyro.gx, gyro.gy, gyro.gz, + dt, dx, dy); + + // Only actually move the pointer while this window is topmost, enabled and connected. + if (!ctx->isEnabled || !ctx->hidDevice) return; + + // Clutch: holding both buttons freezes the pointer so the user can reposition the device + // without moving the cursor. The pipeline above still runs, so nothing lurches on release. + if ((ctx->buttons & 0x03) == 0x03) return; + + if (!moved) return; + sendMouseReport(ctx, static_cast(dx), static_cast(dy)); +} + +// ── Toolbar callbacks ───────────────────────────────────────────────────────── + +// Toolbar action callbacks: the Tactility toolbar registers these for +// LV_EVENT_SHORT_CLICKED, so they must not filter on the event code. +void onSettingsClicked(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (!ctx) return; + showView(ctx, AirMouseView::Settings); +} + +void onBackClicked(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (!ctx) return; + airMouseRequestSaveSettings(ctx); // persist the sensitivity (the sliderbox only reports value changes) + showView(ctx, AirMouseView::Main); +} + +void onSwitchToggled(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (!ctx) return; + bool enabled = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED); + airMouseRequestToggle(ctx, enabled); +} + +// Widgets can't be rebuilt from inside a widget's own event callback (the rebuild +// deletes that widget mid-dispatch), so the switch happens on the next LVGL tick. +void onViewTimer(lv_timer_t* t) { + auto* ctx = static_cast(lv_timer_get_user_data(t)); + ctx->viewTimer = nullptr; + buildView(ctx); +} + +// ── BT lifecycle ────────────────────────────────────────────────────────────── + +// Restores the radio to the state we found it in when we turned it on ourselves. Does not touch +// the BT event subscription - that's app-lifetime, set up by airMouseInitBt()/torn down by +// teardownBt() at app exit, not per toggle. +void restoreRadioIfNeeded(Context* ctx) { + if (ctx->btDevice && ctx->radioWasOff) bluetooth_set_radio_enabled(ctx->btDevice, false); + ctx->radioWasOff = false; +} + +// Takes the LVGL lock. Everything in this file that runs on the app task (BT events, requests) +// needs it before touching a widget or ctx->hidDevice, which the motion timer reads under it. +bool lockLvgl() { + for (int i = 0; i < 5; i++) { + if (lvgl_try_lock(1000)) return true; + } + LOG_W(TAG, "Failed to acquire LVGL lock"); + return false; +} + +void setSwitchChecked(Context* ctx, bool checked) { + if (!lockLvgl()) return; + if (ctx->switchWidget) { + if (checked) lv_obj_add_state(ctx->switchWidget, LV_STATE_CHECKED); + else lv_obj_remove_state(ctx->switchWidget, LV_STATE_CHECKED); + } + lvgl_unlock(); +} + +void startHid(Context* ctx) { + // Called once the BT radio is confirmed ON (either already was, or just came up). + // Runs on the app's own task - never on the LVGL task. + ctx->radioEnabling = false; + + if (ctx->btDevice) { + // Name before the profile start. The name is also the default BLE identity, and + // bluetooth_hid_device_start() applies it to an already-running radio. + bluetooth_set_device_name(ctx->btDevice, DEVICE_NAME); + } + + ctx->hidDevice = bluetooth_hid_device_get(); + if (!ctx->hidDevice) { + LOG_E(TAG, "BLE HID device unavailable after radio on"); + ctx->isEnabled = false; + setSwitchChecked(ctx, false); + return; + } + + error_t err = bluetooth_hid_device_start(ctx->hidDevice, BT_HID_DEVICE_MODE_MOUSE); + if (err != ERROR_NONE) { + LOG_E(TAG, "Failed to start HID mouse: %d", (int)err); + device_put(ctx->hidDevice); + ctx->hidDevice = nullptr; + ctx->isEnabled = false; + setSwitchChecked(ctx, false); + } +} + +void handleBtEvent(Context* ctx, const BtEvent& event) { + if (event.type != BT_EVENT_RADIO_STATE_CHANGED) return; + LOG_I(TAG, "BT radio state: %d", (int)event.radio_state); + + if (event.radio_state == BT_RADIO_STATE_ON) { + if (ctx->radioEnabling && window_manager_get_state(ctx->window) == WINDOW_STATE_GRANTED) { + startHid(ctx); + } + } else if (event.radio_state == BT_RADIO_STATE_OFF && ctx->isEnabled) { + LOG_I(TAG, "BT radio turned off, disabling air mouse"); + if (lvgl_try_lock(1000)) { + if (window_manager_get_state(ctx->window) == WINDOW_STATE_GRANTED && ctx->switchWidget) { + lv_obj_remove_state(ctx->switchWidget, LV_STATE_CHECKED); + } + if (ctx->hidDevice) device_put(ctx->hidDevice); + ctx->hidDevice = nullptr; + ctx->isEnabled = false; + ctx->radioEnabling = false; + ctx->buttons = 0; + lvgl_unlock(); + } + } +} + +void teardownBt(Context* ctx) { + // Unsubscribe FIRST - stops any in-flight BT events from firing against our (possibly + // already freed) UI widget pointers after this returns. + if (ctx->btDevice) bluetooth_event_unsubscribe(ctx->btDevice, &ctx->btEventSub); + // Stop HID before restoring the radio. A profile left active outlives the app: it keeps + // advertising, so the paired PC reconnects to a device that has no app behind it. + // bluetooth_hid_device_stop() is safe from this task now (it no longer touches the GATT + // registry, that is deferred to the host task on disconnect). + // ctx->isEnabled was cleared by airMouseTeardown(), so the motion timer can't race us + // for long; the LVGL lock is still the documented way to own ctx->hidDevice here. + if (lockLvgl()) { + if (ctx->hidDevice) { + if (ctx->btDevice && bluetooth_is_radio_on_or_pending(ctx->btDevice)) { + bluetooth_hid_device_stop(ctx->hidDevice); + } + device_put(ctx->hidDevice); + ctx->hidDevice = nullptr; + } + lvgl_unlock(); + } else if (ctx->hidDevice) { + // Lock not acquired: don't touch the HID session the motion timer may be using, + // but still release our reference. + device_put(ctx->hidDevice); + ctx->hidDevice = nullptr; + } + restoreRadioIfNeeded(ctx); + if (ctx->btDevice) device_put(ctx->btDevice); + ctx->btDevice = nullptr; +} + +} // namespace + +// ── Shared state used by more than one view ─────────────────────────────────── + +void applySettings(Context* ctx) { + airmouse::MotionConfig config; + config.sensitivity = static_cast(ctx->sensitivityPercent) / 100.0f; + config.useOrientation = ctx->useOrientation; + config.invertX = (ctx->invertMode & 0x01) != 0; + config.invertY = (ctx->invertMode & 0x02) != 0; + config.autoClutch = ctx->autoClutch; + ctx->motion.configure(config); +} + +// Settings are written on the kernel's file_ops task: this is queued from LVGL event handlers, +// whose task stack is in PSRAM and must not touch flash. +struct SaveSettingsJob { + char path[192]; + int32_t sensitivityPercent; + int32_t invertMode; + bool useOrientation; + bool autoClutch; + int32_t mountMatrix[9]; +}; + +void saveSettingsTask(void* context) { + auto* job = static_cast(context); + Preferences* prefs = preferences_open(job->path); + if (prefs) { + preferences_put_int32(prefs, PREF_SENSITIVITY, job->sensitivityPercent); + preferences_put_int32(prefs, PREF_INVERT, job->invertMode); + preferences_put_bool(prefs, PREF_ORIENTATION, job->useOrientation); + preferences_put_bool(prefs, PREF_AUTOCLUTCH, job->autoClutch); + for (int i = 0; i < 9; i++) { + char key[8]; + snprintf(key, sizeof(key), "%s%d", PREF_MOUNT, i); + preferences_put_int32(prefs, key, job->mountMatrix[i]); + } + preferences_close(prefs); + } + delete job; +} + +// Snapshots the settings into a job that owns its data, so the work stays valid even if the app +// is torn down before it runs. +void queueSaveSettings(Context* ctx) { + auto* job = new SaveSettingsJob {}; + if (!getSettingsPath(job->path, sizeof(job->path))) { + delete job; + return; + } + job->sensitivityPercent = ctx->sensitivityPercent; + job->invertMode = ctx->invertMode; + job->useOrientation = ctx->useOrientation; + job->autoClutch = ctx->autoClutch; + for (int i = 0; i < 9; i++) job->mountMatrix[i] = ctx->extMountMatrix.m[i]; + if (file_ops_queue(saveSettingsTask, job, nullptr) != ERROR_NONE) { + LOG_E(TAG, "Failed to queue settings save"); + delete job; + } +} + +void sendMouseReport(Context* ctx, int8_t dx, int8_t dy) { + if (!ctx->hidDevice || !bluetooth_hid_device_is_connected(ctx->hidDevice)) return; + uint8_t report[4] = { ctx->buttons, static_cast(dx), static_cast(dy), 0 }; + bluetooth_hid_device_send_mouse(ctx->hidDevice, report, sizeof(report)); +} + +void handleSwitchToggle(Context* ctx, bool enabled) { + // Runs on the app's own task (see airMouseRequestToggle()) so the flash access below can't + // happen on the LVGL task. + LOG_I(TAG, "Switch: %s", enabled ? "ON" : "OFF"); + ctx->isEnabled = enabled; + + if (enabled) { + if (!ctx->btDevice) { + LOG_E(TAG, "No Bluetooth device found"); + ctx->isEnabled = false; + setSwitchChecked(ctx, false); + return; + } + + // Fresh bias calibration each time the pointer is switched on (gyro bias + // drifts with temperature between sessions). The motion processor is owned by the LVGL + // motion timer, so this has to happen under its lock now that we run on the app task. + if (lockLvgl()) { + ctx->motion.reset(); + ctx->lastMotionTick = 0; + lvgl_unlock(); + } + + BtRadioState radioState; + bluetooth_get_radio_state(ctx->btDevice, &radioState); + + // Name before the profile start. In the radio-off path it also has to be set before + // enabling, because the identity is resolved while the radio comes up. + bluetooth_set_device_name(ctx->btDevice, DEVICE_NAME); + + if (radioState == BT_RADIO_STATE_ON) { + // Radio already up - start HID immediately. + ctx->radioWasOff = false; + startHid(ctx); + } else { + // Turn the radio on; startHid() will be called from handleBtEvent() + // once BT_RADIO_STATE_ON fires. Our BT event subscription (airMouseInitBt()) + // was already claimed before the app's main loop started waiting, so that + // wakeup won't be missed. + LOG_I(TAG, "BT radio not on (state=%d), enabling...", (int)radioState); + ctx->radioWasOff = true; + ctx->radioEnabling = true; + bluetooth_set_radio_enabled(ctx->btDevice, true); + } + } else { + ctx->radioEnabling = false; + // Explicit user toggle-off: stop HID cleanly (safe here since the user intentionally + // disabled, so no race with app teardown). The LVGL lock protects ctx->hidDevice, which + // the motion timer dereferences while sending a report. + if (lockLvgl()) { + if (ctx->hidDevice) { + if (ctx->btDevice && bluetooth_is_radio_on_or_pending(ctx->btDevice)) { + bluetooth_hid_device_stop(ctx->hidDevice); + } + device_put(ctx->hidDevice); + } + ctx->hidDevice = nullptr; + lvgl_unlock(); + } + ctx->buttons = 0; + restoreRadioIfNeeded(ctx); + } +} + +// ── Views ───────────────────────────────────────────────────────────────────── + +void showView(Context* ctx, AirMouseView view) { + ctx->view = view; + if (ctx->viewTimer == nullptr) { + ctx->viewTimer = lv_timer_create(onViewTimer, 0, ctx); + lv_timer_set_repeat_count(ctx->viewTimer, 1); + } +} + +// Rebuilds the window widgets for ctx->view: the toolbar (both views need it as the first +// child of the column) then the content container the view fills. Unrelated widget pointers +// are nulled first, so the motion timer and callbacks can never touch a freed widget. +void buildView(Context* ctx) { + if (!ctx->root) return; + + lv_coord_t w = lv_display_get_horizontal_resolution(nullptr); + lv_coord_t h = lv_display_get_vertical_resolution(nullptr); + int gap = (w < 280 || h < 180) ? 2 : 4; + + lv_obj_clean(ctx->root); + ctx->toolbar = nullptr; + ctx->content = nullptr; + ctx->switchWidget = nullptr; + ctx->gyroLabel = nullptr; + ctx->leftClickBtn = nullptr; + ctx->rightClickBtn = nullptr; + ctx->sensitivityBox = nullptr; + ctx->invertBtn = nullptr; + ctx->calibrateBtn = nullptr; + ctx->orientationSwitch = nullptr; + ctx->autoClutchSwitch = nullptr; + + if (ctx->view == AirMouseView::Settings) { + ctx->toolbar = lvgl_toolbar_create(ctx->root, "Settings"); + lvgl_toolbar_set_nav_action(ctx->toolbar, LV_SYMBOL_LEFT, onBackClicked, ctx); + } else { + ctx->toolbar = lvgl_toolbar_create(ctx->root, "Air Mouse"); + ctx->switchWidget = lvgl_toolbar_add_switch_action(ctx->toolbar); + if (ctx->isEnabled) lv_obj_add_state(ctx->switchWidget, LV_STATE_CHECKED); + lv_obj_add_event_cb(ctx->switchWidget, onSwitchToggled, LV_EVENT_VALUE_CHANGED, ctx); + lvgl_toolbar_add_text_button_action(ctx->toolbar, LV_SYMBOL_SETTINGS, onSettingsClicked, ctx); + + // Reflect an already-running radio instead of showing an idle switch: the app should + // serve as soon as it is opened, like MediaKeys and MidiController do. btDevice was + // acquired by airMouseInitBt() before the first view was built. The toggle runs on the + // app's own task, exactly like a manual one. + if (!ctx->isEnabled && ctx->btDevice && device_is_ready(ctx->btDevice)) { + BtRadioState radioState; + if (bluetooth_get_radio_state(ctx->btDevice, &radioState) == ERROR_NONE && + radioState == BT_RADIO_STATE_ON) { + lv_obj_add_state(ctx->switchWidget, LV_STATE_CHECKED); + airMouseRequestToggle(ctx, true); + } + } + } + + ctx->content = lv_obj_create(ctx->root); + lv_obj_set_width(ctx->content, LV_PCT(100)); + lv_obj_set_flex_grow(ctx->content, 1); + lv_obj_set_flex_flow(ctx->content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(ctx->content, gap, 0); + lv_obj_set_style_border_width(ctx->content, 0, 0); + lv_obj_set_style_radius(ctx->content, 0, 0); + + if (!ctx->imuDevice && !ctx->extI2cCtrl) { + lv_obj_t* wrapperWidget = lv_obj_create(ctx->content); + lv_obj_set_width(wrapperWidget, LV_PCT(100)); + lv_obj_set_flex_grow(wrapperWidget, 1); + lv_obj_set_style_pad_all(wrapperWidget, 0, 0); + lv_obj_set_style_border_width(wrapperWidget, 0, 0); + lv_obj_set_style_bg_opa(wrapperWidget, LV_OPA_TRANSP, 0); + lv_obj_set_scrollable(wrapperWidget, false); + // Show error in a centered label + lv_obj_t* errLabel = lv_label_create(wrapperWidget); + lv_label_set_text(errLabel, "No IMU found"); + lv_obj_center(errLabel); + return; + } + + if (ctx->view == AirMouseView::Settings) { + settingsViewCreate(ctx); + } else { + mainViewCreate(ctx); + } +} + +void airMouseCreateWidgets(lv_obj_t* parent, void* userData) { + auto* ctx = static_cast(userData); + ctx->root = parent; + ctx->view = AirMouseView::Main; + + lv_coord_t w = lv_display_get_horizontal_resolution(nullptr); + lv_coord_t h = lv_display_get_vertical_resolution(nullptr); + bool isSmall = w < 280 || h < 180; + int pad = isSmall ? 2 : 4; + int gap = isSmall ? 2 : 4; + + lv_obj_set_scrollable(parent, false); + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(parent, pad, 0); + lv_obj_set_style_pad_row(parent, gap, 0); + + buildView(ctx); + + if (!ctx->motionTimer) { + ctx->motionTimer = lv_timer_create(onMotionTimer, 30, ctx); // ~33Hz for responsive mouse + } +} + +void airMouseTeardown(Context* ctx) { + ctx->radioEnabling = false; + ctx->isEnabled = false; + // Final snapshot. The queued job owns its data, so it still lands after the app is gone. + queueSaveSettings(ctx); + teardownBt(ctx); + if (ctx->imuDevice) { device_put(ctx->imuDevice); ctx->imuDevice = nullptr; } + // motionTimer is app-owned, not part of the window's widget subtree, so + // window_manager_remove() never touches it - still safe (and necessary) to delete here. + if (ctx->motionTimer) { + lv_timer_delete(ctx->motionTimer); + ctx->motionTimer = nullptr; + } + if (ctx->viewTimer) { + lv_timer_delete(ctx->viewTimer); + ctx->viewTimer = nullptr; + } + + ctx->toolbar = nullptr; + ctx->content = nullptr; + ctx->switchWidget = nullptr; + ctx->gyroLabel = nullptr; + ctx->leftClickBtn = nullptr; + ctx->rightClickBtn = nullptr; + ctx->sensitivityBox = nullptr; + ctx->invertBtn = nullptr; + ctx->calibrateBtn = nullptr; + ctx->orientationSwitch = nullptr; + ctx->autoClutchSwitch = nullptr; + ctx->root = nullptr; +} + +// ── App lifecycle ────────────────────────────────────────────────────────────── + +bool airMouseInitBt(Context* ctx) { + loadSettings(ctx); + discoverImu(ctx); + if (!ctx->imuDevice && !ctx->extI2cCtrl) { + LOG_W(TAG, "No IMU found"); + } + + // ble0 is started for the process lifetime once enabled in the devicetree - just look it up, + // no device_start() needed. + if (device_get_first_by_type(&BLUETOOTH_TYPE, &ctx->btDevice) != ERROR_NONE) { + LOG_E(TAG, "No Bluetooth device found"); + ctx->btDevice = nullptr; + return false; + } + // Must claim this subscription's bit before the app's main loop makes its first + // task_event_group_wait_any() call - a bit claimed mid-wait isn't included until the next + // call, so a radio-on event fired right after a late subscribe could go unnoticed forever. + if (bluetooth_event_subscribe(ctx->btDevice, &ctx->btEventSub, ctx->eventGroup) != ERROR_NONE) { + LOG_E(TAG, "Failed to subscribe to BT events"); + device_put(ctx->btDevice); + ctx->btDevice = nullptr; + return false; + } + return true; +} + +void airMouseProcessBtEvents(Context* ctx) { + if (ctx->btDevice == nullptr) return; + BtEvent event {}; + while (bluetooth_event_poll(&ctx->btEventSub, &event) == ERROR_NONE) { + handleBtEvent(ctx, event); + } +} + +void airMouseRequestToggle(Context* ctx, bool enabled) { + ctx->pendingToggle.store(enabled ? 1 : 0); + if (ctx->requestBit != 0) task_event_group_signal(ctx->eventGroup, ctx->requestBit); +} + +void airMouseRequestSaveSettings(Context* ctx) { + queueSaveSettings(ctx); +} + +void airMouseProcessRequests(Context* ctx) { + int toggle = ctx->pendingToggle.exchange(-1); + if (toggle >= 0) handleSwitchToggle(ctx, toggle != 0); +} diff --git a/Apps/AirMouse/main/Source/AirMouse.h b/Apps/AirMouse/main/Source/AirMouse.h new file mode 100644 index 0000000..8ddf6a3 --- /dev/null +++ b/Apps/AirMouse/main/Source/AirMouse.h @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "Motion.h" +#include +#include + +// Which page of the single window is showing. +enum class AirMouseView { + Main, + Settings, +}; + +struct Context { + AppInstanceId appInstanceId = 0; + WindowId window = 0; + struct TaskEventGroup* eventGroup = nullptr; // borrowed from main(); outlives ctx + + // UI elements + lv_obj_t* root = nullptr; // the window's content parent, rebuilt on view change + lv_obj_t* toolbar = nullptr; + lv_obj_t* content = nullptr; // below the toolbar; the visible view fills this + AirMouseView view = AirMouseView::Main; + lv_obj_t* switchWidget = nullptr; + lv_obj_t* gyroLabel = nullptr; + lv_obj_t* leftClickBtn = nullptr; + lv_obj_t* rightClickBtn = nullptr; + lv_timer_t* motionTimer = nullptr; + lv_obj_t* sensitivityBox = nullptr; // sliderbox: slider + "-"/"+" steps + its own value label + lv_obj_t* invertBtn = nullptr; + lv_obj_t* calibrateBtn = nullptr; + lv_obj_t* orientationSwitch = nullptr; + lv_obj_t* autoClutchSwitch = nullptr; + lv_timer_t* viewTimer = nullptr; // one-shot: deferred view rebuild + + // IMU device (device tree, persistent across widget rebuilds; released in airMouseTeardown) + Device* imuDevice = nullptr; + // External I2C fallback (used when no device-tree entry exists - a bare/Unit-style MPU6886 + // plugged into an exposed I2C bus, any device can use it) + Device* extI2cCtrl = nullptr; + uint8_t extImuAddr = 0; + // Sensor -> device frame transform for the external module (the devicetree drivers apply + // their own mount-matrix; a bare chip has none, so we keep one here). Persisted. + airmouse::MountMatrix extMountMatrix {}; + + // Motion pipeline (bias calibration, orientation mapping, filtering, sub-pixel carry) + airmouse::MotionProcessor motion {}; + uint32_t lastMotionTick = 0; + + // Motion settings (persisted) + int sensitivityPercent = 100; // 25..300 + int invertMode = 0; // bit 0 = invert X, bit 1 = invert Y + bool useOrientation = true; + bool autoClutch = false; // freeze the pointer while repositioning the device + + // HAL device handles + struct Device* btDevice = nullptr; + struct Device* hidDevice = nullptr; + struct BtEventSubscription btEventSub {}; + + // State - accessed from both LVGL thread and the app's own task (BT event poll loop) + std::atomic isEnabled {false}; + std::atomic radioEnabling {false}; // true while waiting for radio to come ON + std::atomic radioWasOff {false}; // true if we turned the radio on (restore on exit) + + // Work that can touch flash must not run on the LVGL task: its stack is in PSRAM and flash + // access disables the cache. The BT toggle runs on the app's own task (internal RAM, already + // polling BT events); settings are written by the kernel's file_ops task. + uint32_t requestBit = 0; // claimed in main() before the first wait + std::atomic pendingToggle {-1}; // -1 = none, 0 = off, 1 = on + + // Mouse HID state + uint8_t buttons = 0; // bit 0 = left, bit 1 = right +}; + +/** window_manager_create()'s WindowCreateWidgetsFn - @a userData is the Context* for this instance. */ +void airMouseCreateWidgets(lv_obj_t* parent, void* userData); + +/** Looks up the IMU and BT devices and subscribes to BT events, claiming a bit in ctx->eventGroup. + * Must be called once, before the app's main loop starts blocking on that event group (its bit + * has to already be claimed by the first task_event_group_wait_any() call - see that function's + * warning about bits claimed mid-wait). @return true if a BT device was found (IMU absence is + * reported separately, in the UI, since the app can still show useful state without one). */ +bool airMouseInitBt(Context* ctx); + +/** Drains any BT events queued for ctx and reacts to them (radio state, HID profile state). + * Call from the app's main loop after task_event_group_wait_any() returns. */ +void airMouseProcessBtEvents(Context* ctx); + +/** Asks the app task to turn the pointer on or off. Call from an LVGL event handler. */ +void airMouseRequestToggle(Context* ctx, bool enabled); + +/** Queues a settings save on the kernel file_ops task. Call from an LVGL event handler. */ +void airMouseRequestSaveSettings(Context* ctx); + +/** Runs the requests queued by the two functions above. Call from the app's main loop. */ +void airMouseProcessRequests(Context* ctx); + +/** Removes the BT event subscription, stops HID, restores radio state, releases the IMU device + * reference, releases widget-tracking state. Call once, after the window has been torn down. */ +void airMouseTeardown(Context* ctx); + +// ── Views ───────────────────────────────────────────────────────────────────── +// buildView() owns the toolbar and the content container (both views need them in that +// order); each view file only fills ctx->content - see MainView.cpp and SettingsView.cpp. + +/** Rebuilds the window's widgets (toolbar + content) for ctx->view. */ +void buildView(Context* ctx); + +/** Shows another view. The switch happens on the next LVGL tick, because the caller is + * usually inside an event of a widget that the rebuild deletes. */ +void showView(Context* ctx, AirMouseView view); + +/** Fills ctx->content with the main view: the large left/right click pads. */ +void mainViewCreate(Context* ctx); + +/** Fills ctx->content with the settings view: status, sensitivity, direction, switches, calibrate. */ +void settingsViewCreate(Context* ctx); + +// ── Shared state used by more than one view ─────────────────────────────────── + +/** Pushes ctx's persisted motion settings into the pipeline. */ +void applySettings(Context* ctx); + +/** Turns the pointer on or off: the toolbar switch, and the BT radio going away. */ +void handleSwitchToggle(Context* ctx, bool enabled); + +/** Sends a mouse report with ctx->buttons and the given movement. */ +void sendMouseReport(Context* ctx, int8_t dx, int8_t dy); diff --git a/Apps/AirMouse/main/Source/MainView.cpp b/Apps/AirMouse/main/Source/MainView.cpp new file mode 100644 index 0000000..bb5070c --- /dev/null +++ b/Apps/AirMouse/main/Source/MainView.cpp @@ -0,0 +1,81 @@ +#include "AirMouse.h" + +#include + +namespace { + +void onLeftPressed(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_PRESSED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->buttons |= 0x01; + sendMouseReport(ctx, 0, 0); +} + +void onLeftReleased(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_RELEASED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->buttons &= ~0x01; + sendMouseReport(ctx, 0, 0); +} + +void onRightPressed(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_PRESSED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->buttons |= 0x02; + sendMouseReport(ctx, 0, 0); +} + +void onRightReleased(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_RELEASED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->buttons &= ~0x02; + sendMouseReport(ctx, 0, 0); +} + +/** + * One of the two click pads: it shares the row with its sibling and fills its height. + * + * Zeroes the button's own margin on purpose. The theme gives buttons a margin to leave room + * for their focus ring, but flex grow does not take a growing item's own margins into account + * when it divides the row (lv_flex.c), while placement does add them - so two grown pads push + * the right one out of the row by 3x that margin (visible as the right pad running off the + * display on narrow screens). The row reserves that room for both of them instead. + */ +lv_obj_t* createClickPad(lv_obj_t* parent, const char* title, + lv_event_cb_t onPressed, lv_event_cb_t onReleased, Context* ctx) { + lv_obj_t* pad = lv_btn_create(parent); + lv_obj_set_style_margin_all(pad, 0, 0); + lv_obj_set_flex_grow(pad, 1); + lv_obj_set_height(pad, LV_PCT(100)); + lv_obj_add_event_cb(pad, onPressed, LV_EVENT_PRESSED, ctx); + lv_obj_add_event_cb(pad, onReleased, LV_EVENT_RELEASED, ctx); + lv_obj_t* label = lv_label_create(pad); + lv_label_set_text(label, title); + lv_obj_center(label); + return pad; +} + +} // namespace + +void mainViewCreate(Context* ctx) { + lv_coord_t w = lv_display_get_horizontal_resolution(nullptr); + lv_coord_t h = lv_display_get_vertical_resolution(nullptr); + int gap = (w < 280 || h < 180) ? 2 : 4; + + // Dragging a pad must not scroll the window, so the content stays fixed here. + lv_obj_set_scrollable(ctx->content, false); + + lv_obj_t* row = lv_obj_create(ctx->content); + lv_obj_remove_style_all(row); + lv_obj_set_width(row, LV_PCT(100)); + lv_obj_set_flex_grow(row, 1); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + // Room for the pads' focus ring (see createClickPad) and the gap to the window edge. + lv_obj_set_style_pad_hor(row, gap, 0); + lv_obj_set_style_pad_column(row, gap, 0); + lv_obj_set_scrollable(row, false); + + ctx->leftClickBtn = createClickPad(row, "Left", onLeftPressed, onLeftReleased, ctx); + ctx->rightClickBtn = createClickPad(row, "Right", onRightPressed, onRightReleased, ctx); +} diff --git a/Apps/AirMouse/main/Source/Motion.h b/Apps/AirMouse/main/Source/Motion.h new file mode 100644 index 0000000..f8ee692 --- /dev/null +++ b/Apps/AirMouse/main/Source/Motion.h @@ -0,0 +1,521 @@ +#pragma once + +// Air-mouse motion pipeline: gyro bias calibration, gravity-based orientation +// mapping, smoothing, deadzone and sub-pixel accumulation. +// +// All IMU values entering this file are in a single, consistent body frame +// ("device frame"): +X = right, +Y = up (top of the screen), +Z = out of the +// screen toward the user. For devicetree IMUs the driver's mount-matrix has +// already produced that frame; for the bare external MPU6886 fallback the caller +// applies the same convention via MountMatrix. + +#include +#include +#include + +namespace airmouse { + +// Row-major 3x3 matrix using the Linux IIO "mount-matrix" convention: +// x' = m[0]*x + m[1]*y + m[2]*z +// y' = m[3]*x + m[4]*y + m[5]*z +// z' = m[6]*x + m[7]*y + m[8]*z +// Maps the raw sensor frame onto the device frame described above. +struct MountMatrix { + int32_t m[9] = { 1, 0, 0, 0, 1, 0, 0, 0, 1 }; + + void apply(float& x, float& y, float& z) const { + const float rx = m[0] * x + m[1] * y + m[2] * z; + const float ry = m[3] * x + m[4] * y + m[5] * z; + const float rz = m[6] * x + m[7] * y + m[8] * z; + x = rx; + y = ry; + z = rz; + } +}; + +// One Euro filter: heavy smoothing at low speed, low latency at high speed. +// (Casiez, Roussel & Vogel, CHI 2012.) Ideal for pointer input - it removes the +// jitter/creep of a raw gyro without the lag of a fixed low-pass. +class OneEuroFilter { +public: + void configure(float minCutoff, float beta, float dCutoff) { + minCutoff_ = minCutoff; + beta_ = beta; + dCutoff_ = dCutoff; + } + + void reset() { initialized_ = false; } + + float filter(float value, float dt) { + if (dt <= 0.0f) dt = kFallbackDt; + if (!initialized_) { + initialized_ = true; + xPrev_ = value; + dxPrev_ = 0.0f; + return value; + } + const float dx = (value - xPrev_) / dt; + const float aD = alpha(dCutoff_, dt); + const float dxHat = dxPrev_ + aD * (dx - dxPrev_); + dxPrev_ = dxHat; + const float cutoff = minCutoff_ + beta_ * std::fabs(dxHat); + const float a = alpha(cutoff, dt); + xPrev_ = xPrev_ + a * (value - xPrev_); + return xPrev_; + } + +private: + static constexpr float kFallbackDt = 1.0f / 33.0f; + + static float alpha(float cutoff, float dt) { + const float tau = 1.0f / (2.0f * 3.14159265f * cutoff); + return 1.0f / (1.0f + tau / dt); + } + + float minCutoff_ = 2.0f; + float beta_ = 0.05f; + float dCutoff_ = 1.0f; + float xPrev_ = 0.0f; + float dxPrev_ = 0.0f; + bool initialized_ = false; +}; + +struct MotionConfig { + float sensitivity = 1.0f; // user multiplier (1.0 = stock feel) + bool useOrientation = true; // gravity-based axis mapping + bool invertX = false; // flip horizontal direction + bool invertY = false; // flip vertical direction + // Freeze the pointer automatically while the device is being physically + // repositioned. The manual clutch (both click buttons) is impossible on a + // device with no touch screen, so this is the only way to "let go" there. + bool autoClutch = false; +}; + +class MotionProcessor { +public: + // Pointer response: pixels per (°/s) per second. Derived from the original + // fixed-30ms constants (0.48 = 16.0 * 0.030, 0.285 = 9.5 * 0.030), now made + // frame-rate independent. + static constexpr float kGainHorizontal = 16.0f; + static constexpr float kGainVertical = 9.5f; + + void configure(const MotionConfig& config) { config_ = config; } + + // Call when the pointer is (re)enabled: clears filters/accumulators and + // restarts bias calibration. No pointer output is produced until a bias + // estimate exists. + void reset() { + filterH_.reset(); + filterV_.reset(); + accumX_ = 0.0f; + accumY_ = 0.0f; + bias_[0] = bias_[1] = bias_[2] = 0.0f; + biasValid_ = false; + gravValid_ = false; + autoClutchRun_ = 0; + autoClutchQuiet_ = 0.0f; + autoClutchActive_ = false; + beginCalibration(); + } + + void beginCalibration() { + calibCount_ = 0; + calibTicks_ = 0; + calibWindows_ = 0; + calibrating_ = true; + } + + bool isCalibrating() const { return calibrating_; } + bool hasBias() const { return biasValid_; } + // True while the pointer is being held back because the device is being moved + // (only ever true with config_.autoClutch on). + bool isAutoClutching() const { return autoClutchActive_; } + float biasMagnitude() const { + return std::sqrt(bias_[0] * bias_[0] + bias_[1] * bias_[1] + bias_[2] * bias_[2]); + } + // True when the last sample had enough gravity for orientation mapping. + bool orientationActive() const { return orientValid_ && config_.useOrientation; } + + // Feeds one sample (accel in g, gyro in °/s, both device frame; dt in seconds). + // Returns true and fills outDx/outDy when a pointer delta should be sent. + bool update(float ax, float ay, float az, + float gx, float gy, float gz, + float dt, int& outDx, int& outDy) { + if (dt <= 0.0f) dt = 1.0f / 33.0f; + + if (!updateBias(gx, gy, gz)) { + return false; // still calibrating - don't steer with an unknown bias + } + gx -= bias_[0]; + gy -= bias_[1]; + gz -= bias_[2]; + + updateAutoClutch(ax, ay, az, dt); + + float rateH; + float rateV; + if (config_.useOrientation && computeOrientation(ax, ay, az, gx, gy, gz, dt)) { + // World-referenced: yaw about the gravity axis, pitch about the + // horizontal axis the device "points" along. Consistent however the + // device is held (flat, upright, portrait, landscape). + rateH = yaw_; + rateV = pitch_; + } else { + // Legacy flat-hold mapping (the original layout of this app). + rateH = gz; + rateV = -gy; + } + + if (config_.invertX) rateH = -rateH; + if (config_.invertY) rateV = -rateV; + + // Radial deadzone in °/s with a continuous, direction-preserving onset - + // no per-axis "sticky cross". + const float rateMag = std::sqrt(rateH * rateH + rateV * rateV); + if (rateMag < kDeadzone) { + rateH = 0.0f; + rateV = 0.0f; + } else { + const float scale = (rateMag - kDeadzone) / rateMag; + rateH *= scale; + rateV *= scale; + } + + rateH = filterH_.filter(rateH, dt); + rateV = filterV_.filter(rateV, dt); + + float dxF = rateH * kGainHorizontal * config_.sensitivity * dt; + float dyF = rateV * kGainVertical * config_.sensitivity * dt; + + // Sub-pixel accumulation: keep the fractional part so slow motion still + // moves, instead of rounding every tick to zero. + accumX_ += dxF; + accumY_ += dyF; + int dx = std::clamp(static_cast(accumX_), -kMaxStep, kMaxStep); + int dy = std::clamp(static_cast(accumY_), -kMaxStep, kMaxStep); + accumX_ -= static_cast(dx); + accumY_ -= static_cast(dy); + // The leftover is normally the sub-pixel fraction (<1). If the per-tick + // clamp fired, discard the pent-up excess instead of "coasting" later. + accumX_ = std::clamp(accumX_, -1.0f, 1.0f); + accumY_ = std::clamp(accumY_, -1.0f, 1.0f); + + // Auto-clutch: the device is being moved, not steered - hold the pointer + // still. The filters and the sub-pixel carry above keep running and drain + // every tick, so nothing has piled up to lurch forward when it releases. + if (autoClutchActive_) return false; + + if (dx == 0 && dy == 0) return false; + outDx = dx; + outDy = dy; + return true; + } + +private: + // Noise floor and calibration thresholds (°/s). + static constexpr float kDeadzone = 1.5f; + static constexpr int kMaxStep = 40; + // Bias calibration window: the estimate is the per-axis median of that many samples. + // The window is retried while the samples keep scattering, so that a moving hand is not + // baked into the bias, and given up on after kCalibMaxTicks. + static constexpr int kCalibSamples = 48; // ~1.5s at 33Hz + static constexpr float kCalibQuietRange = 4.0f; // "still holding it" scatter around the median + static constexpr int kCalibMaxTicks = 150; // ~4.5s: use the best estimate we have + static constexpr int kCalibMaxWindows = 4; + static constexpr float kDriftMaxMag = 1.2f; + static constexpr float kDriftRate = 0.01f; + // Below this |accel| (g) we don't trust the gravity direction. + static constexpr float kMinAccelMag = 0.3f; + // Gravity tracking: time constant (s) of the low-pass that smooths the + // gravity direction, and how far |accel| may deviate from 1 g before we stop + // trusting it (linear acceleration from a hand movement). + static constexpr float kGravTau = 0.12f; + static constexpr float kGravTrustRange = 0.20f; + + // Auto-clutch. Steering rotates the device (mostly) in place, so |accel| stays + // near 1 g; picking the device up or carrying it to a new position also + // translates it, which adds linear acceleration on top of gravity and pushes + // |accel| away from 1 g. That difference is the whole signal: enough sustained + // "off 1 g" samples = being moved = freeze. A brisk wrist rotation can add a + // little centripetal/tangential acceleration (~0.2 g worst case), which is why + // the threshold sits above that and below a whole-arm carry (0.3-1 g). + // + // kAutoClutchRange is the main tuning knob: raise it if brisk steering ever + // freezes the pointer, lower it if repositioning still flings it. + static constexpr float kAutoClutchRange = 0.25f; // |accel| deviation (g) counted as "being moved" + static constexpr int kAutoClutchEngageTicks = 2; // consecutive such samples before freezing (~60ms) + static constexpr float kAutoClutchReleaseTime = 0.25f; // quiet time (s) before moving pointer again + + // Returns false while a bias estimate is still being formed. + // + // The estimate is a per-axis median rather than "the mean of the samples whose magnitude + // stayed under X °/s". That threshold only works while the sensor's bias itself is small: + // the samples it is meant to reject are the ones where the user's hand moved, but a sensor + // biased by more than the threshold puts *every* sample over it, so nothing can be measured + // at all and the estimate collapses to a zero bias (a breakout MPU6886 may sit at several + // °/s; the datasheet allows ±20). A median does not care how large the bias is, and it + // ignores the moving samples as long as they are a minority of the window. + bool updateBias(float gx, float gy, float gz) { + if (calibrating_) { + if (calibCount_ < kCalibSamples) { + calibBuf_[0][calibCount_] = gx; + calibBuf_[1][calibCount_] = gy; + calibBuf_[2][calibCount_] = gz; + calibCount_++; + } + calibTicks_++; + + const bool windowFull = calibCount_ >= kCalibSamples; + const bool outOfTime = calibTicks_ >= kCalibMaxTicks; + if (windowFull || outOfTime) { + const bool still = measureBias(); + // The hand was still moving: throw this window away and look at a fresh one, + // unless we are out of patience (the pointer must not stay parked for ever). + if (!still && !outOfTime && ++calibWindows_ < kCalibMaxWindows) { + calibCount_ = 0; + return false; + } + biasValid_ = true; + calibrating_ = false; + return true; + } + return false; + } + + if (biasValid_) { + // At rest, gently follow slow thermal drift of the bias. Note this follows a + // drifting bias *continuously* (it only ever has to move by a fraction of the + // range per second), which is what keeps a warming-up sensor from creeping. + const float dx = gx - bias_[0]; + const float dy = gy - bias_[1]; + const float dz = gz - bias_[2]; + if (std::sqrt(dx * dx + dy * dy + dz * dz) < kDriftMaxMag) { + bias_[0] += kDriftRate * dx; + bias_[1] += kDriftRate * dy; + bias_[2] += kDriftRate * dz; + } + } + return true; + } + + // Per-axis median of the collected window into bias_. Returns true when enough of the + // window sits close to that median to call the device "held still" (the rest was the hand + // moving, which the median has already ignored). + bool measureBias() { + if (calibCount_ == 0) { + bias_[0] = bias_[1] = bias_[2] = 0.0f; // no samples at all (read failures) + return true; + } + + for (int axis = 0; axis < 3; axis++) { + float sorted[kCalibSamples]; + for (int i = 0; i < calibCount_; i++) sorted[i] = calibBuf_[axis][i]; + for (int i = 1; i < calibCount_; i++) { + const float value = sorted[i]; + int j = i - 1; + while (j >= 0 && sorted[j] > value) { + sorted[j + 1] = sorted[j]; + j--; + } + sorted[j + 1] = value; + } + bias_[axis] = sorted[calibCount_ / 2]; + } + + int quiet = 0; + for (int i = 0; i < calibCount_; i++) { + const float dx = calibBuf_[0][i] - bias_[0]; + const float dy = calibBuf_[1][i] - bias_[1]; + const float dz = calibBuf_[2][i] - bias_[2]; + if (std::sqrt(dx * dx + dy * dy + dz * dz) < kCalibQuietRange) quiet++; + } + // More than half the window has to sit near the median: a genuine "hold it still" sends + // nearly every sample there, while a sustained rotation (carrying the device across the + // room) leaves a large part of the window scattered, so a fresh window is worth trying. + return quiet * 2 > calibCount_; + } + + // Freezes the pointer while the device is being carried to a new position (so + // the user can "let go" without a button chord) and releases it once the device + // is quiet again. See the kAutoClutch* constants for the reasoning. + void updateAutoClutch(float ax, float ay, float az, float dt) { + if (!config_.autoClutch) { + autoClutchRun_ = 0; + autoClutchQuiet_ = 0.0f; + autoClutchActive_ = false; + return; + } + + const float mag = std::sqrt(ax * ax + ay * ay + az * az); + const float dev = std::fabs(mag - 1.0f); + + if (dev > kAutoClutchRange) { + autoClutchQuiet_ = 0.0f; + if (autoClutchRun_ < kAutoClutchEngageTicks) autoClutchRun_++; + if (autoClutchRun_ >= kAutoClutchEngageTicks) autoClutchActive_ = true; + } else { + autoClutchRun_ = 0; + if (autoClutchActive_) { + autoClutchQuiet_ += dt; + if (autoClutchQuiet_ >= kAutoClutchReleaseTime) { + autoClutchActive_ = false; + autoClutchQuiet_ = 0.0f; + // The gravity estimate was held (untrusted) for the whole move, + // so it still points the old way. Drop it and re-seed from the + // now-quiet device, so the axis mapping is correct the instant + // the pointer starts moving again instead of swinging over. + gravValid_ = false; + } + } + } + } + + // Low-pass estimates the gravity direction while the device is quiet. When + // the accelerometer magnitude leaves 1 g (a hand movement adds linear + // acceleration) the reading is contaminated, so the estimate is held instead + // of followed - otherwise the axis mapping would swing mid-gesture and steer + // the pointer the wrong way while the user physically reorients the device. + void updateGravity(float ax, float ay, float az, float dt) { + const float mag = std::sqrt(ax * ax + ay * ay + az * az); + if (mag < kMinAccelMag) return; // free-fall / unusable + const float nx = ax / mag; + const float ny = ay / mag; + const float nz = az / mag; + + if (!gravValid_) { + gravX_ = nx; + gravY_ = ny; + gravZ_ = nz; + gravValid_ = true; + return; + } + + const float dev = std::fabs(mag - 1.0f); + float trust = 1.0f - dev / kGravTrustRange; + if (trust < 0.0f) trust = 0.0f; + else if (trust > 1.0f) trust = 1.0f; + if (trust <= 0.0f) return; + + const float alpha = trust * (1.0f - std::exp(-dt / kGravTau)); + float bx = gravX_ + alpha * (nx - gravX_); + float by = gravY_ + alpha * (ny - gravY_); + float bz = gravZ_ + alpha * (nz - gravZ_); + const float bm = std::sqrt(bx * bx + by * by + bz * bz); + if (bm > 1e-3f) { + gravX_ = bx / bm; + gravY_ = by / bm; + gravZ_ = bz / bm; + } + } + + // Derives yaw_/pitch_ (°/s) from the gyro and the gravity direction. + // Returns false if gravity isn't usable (free-fall / bad read). + bool computeOrientation(float ax, float ay, float az, + float gx, float gy, float gz, float dt) { + updateGravity(ax, ay, az, dt); + orientValid_ = gravValid_; + if (!orientValid_) return false; + + const float ux = gravX_; + const float uy = gravY_; + const float uz = gravZ_; // world-up in the device frame (smoothed) + + // "Forward" reference for pitch = the direction the device *points*, i.e. + // the screen normal (+Z) projected onto the horizontal plane. This is the + // right choice held upright (screen vertical), and it's what makes the + // mapping independent of how the device is rotated in the hand. + const float ndot = uz; + float nx = -ndot * ux; + float ny = -ndot * uy; + float nz = 1.0f - ndot * uz; + const float nmag = std::sqrt(nx * nx + ny * ny + nz * nz); + + // When the device is lying flat the screen normal points along gravity and + // gives no horizontal direction, so fall back to the screen top (+Y). + const float tdot = uy; + float tx = -tdot * ux; + float ty = 1.0f - tdot * uy; + float tz = -tdot * uz; + const float tmag = std::sqrt(tx * tx + ty * ty + tz * tz); + + // Hysteresis: prefer the pointing direction whenever it has a usable + // horizontal projection; only fall back to the top edge when nearly flat. + if (usingNormalRef_) { + if (nmag < 0.25f) usingNormalRef_ = false; + } else if (nmag > 0.35f) { + usingNormalRef_ = true; + } + + float fx; + float fy; + float fz; + if (usingNormalRef_ && nmag > 1e-3f) { + // Negated so the pitch sign stays continuous across the flat/upright + // transition (both agree at 45 degrees). + fx = -nx / nmag; + fy = -ny / nmag; + fz = -nz / nmag; + } else if (tmag > 1e-3f) { + fx = tx / tmag; + fy = ty / tmag; + fz = tz / tmag; + } else { + fx = 0.0f; + fy = 0.0f; + fz = 0.0f; + } + + // Horizontal "right" axis = up x forward. + float rx = uy * fz - uz * fy; + float ry = uz * fx - ux * fz; + float rz = ux * fy - uy * fx; + const float rmag = std::sqrt(rx * rx + ry * ry + rz * rz); + if (rmag > 1e-3f) { + rx /= rmag; + ry /= rmag; + rz /= rmag; + } else { + rx = 1.0f; + ry = 0.0f; + rz = 0.0f; + } + + yaw_ = gx * ux + gy * uy + gz * uz; + pitch_ = gx * rx + gy * ry + gz * rz; + return true; + } + + MotionConfig config_ {}; + + OneEuroFilter filterH_ {}; + OneEuroFilter filterV_ {}; + + float accumX_ = 0.0f; + float accumY_ = 0.0f; + + float bias_[3] = { 0.0f, 0.0f, 0.0f }; + float calibBuf_[3][kCalibSamples] = {}; + int calibCount_ = 0; + int calibTicks_ = 0; + int calibWindows_ = 0; + bool calibrating_ = false; + bool biasValid_ = false; + + bool orientValid_ = false; + bool usingNormalRef_ = true; + float yaw_ = 0.0f; + float pitch_ = 0.0f; + + // Smoothed gravity direction (device frame, unit vector). + float gravX_ = 0.0f; + float gravY_ = 0.0f; + float gravZ_ = 0.0f; + bool gravValid_ = false; + + // Auto-clutch state. + int autoClutchRun_ = 0; // consecutive "being moved" samples + float autoClutchQuiet_ = 0.0f; // seconds of quiet accumulated while engaged + bool autoClutchActive_ = false; +}; + +} // namespace airmouse diff --git a/Apps/AirMouse/main/Source/SettingsView.cpp b/Apps/AirMouse/main/Source/SettingsView.cpp new file mode 100644 index 0000000..42f1442 --- /dev/null +++ b/Apps/AirMouse/main/Source/SettingsView.cpp @@ -0,0 +1,142 @@ +#include "AirMouse.h" + +#include +#include + +#include + +namespace { + +lv_obj_t* makeSettingsRow(lv_obj_t* parent) { + lv_obj_t* row = lv_obj_create(parent); + lv_obj_remove_style_all(row); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(row, 8, 0); + lv_obj_set_scrollable(row, false); + return row; +} + +lv_obj_t* makeSettingsSwitch(lv_obj_t* parent, const char* title, bool checked, lv_event_cb_t callback, void* userData) { + lv_obj_t* row = makeSettingsRow(parent); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_t* label = lv_label_create(row); + lv_label_set_text(label, title); + lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_WRAP); + lv_obj_set_flex_grow(label, 1); + lv_obj_t* toggle = lv_switch_create(row); + if (checked) lv_obj_add_state(toggle, LV_STATE_CHECKED); + lv_obj_add_event_cb(toggle, callback, LV_EVENT_VALUE_CHANGED, userData); + return toggle; +} + +const char* invertLabel(int mode) { + switch (mode & 0x03) { + case 0x01: return "Inv:X"; + case 0x02: return "Inv:Y"; + case 0x03: return "Inv:XY"; + default: return "Inv:off"; + } +} + +// Not persisted here: a slider drag fires this for every step it snaps to. onBackClicked() +// and airMouseTeardown() save the settings file instead. +void onSensitivityChanged(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (!ctx) return; + ctx->sensitivityPercent = static_cast(lvgl_sliderbox_get_value(lv_event_get_target_obj(event))); + applySettings(ctx); +} + +void onInvertClicked(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_CLICKED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (!ctx) return; + ctx->invertMode = (ctx->invertMode + 1) & 0x03; + applySettings(ctx); + if (ctx->invertBtn) { + lv_obj_t* label = lv_obj_get_child(ctx->invertBtn, 0); + if (label) lv_label_set_text(label, invertLabel(ctx->invertMode)); + } + airMouseRequestSaveSettings(ctx); +} + +void onCalibrateClicked(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_CLICKED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (!ctx) return; + ctx->motion.beginCalibration(); +} + +void onOrientationToggled(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (!ctx) return; + ctx->useOrientation = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED); + applySettings(ctx); + airMouseRequestSaveSettings(ctx); +} + +void onAutoClutchToggled(lv_event_t* event) { + if (lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return; + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (!ctx) return; + ctx->autoClutch = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED); + applySettings(ctx); + airMouseRequestSaveSettings(ctx); +} + +} // namespace + +void settingsViewCreate(Context* ctx) { + lv_obj_set_scroll_dir(ctx->content, LV_DIR_VER); + + // Status readout (calibration / orientation mode) + ctx->gyroLabel = lv_label_create(ctx->content); + lv_label_set_text(ctx->gyroLabel, "bias:--"); + lv_obj_set_width(ctx->gyroLabel, LV_PCT(100)); + lv_obj_set_style_text_font(ctx->gyroLabel, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + + // Sensitivity: the sliderbox carries its own value label and "-"/"+" steps, so its + // title goes on the line above - a title and a full box don't share a narrow row. + lv_obj_t* sensTitle = lv_label_create(ctx->content); + lv_label_set_text(sensTitle, "Sensitivity (%)"); + ctx->sensitivityBox = lvgl_sliderbox_create(ctx->content, 25, 300, 5, ctx->sensitivityPercent); + lvgl_sliderbox_add_value_changed_cb(ctx->sensitivityBox, onSensitivityChanged, ctx); + + // Pointer direction (cycles Inv: off/X/Y/XY) + lv_obj_t* invRow = makeSettingsRow(ctx->content); + lv_obj_set_flex_align(invRow, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_t* invTitle = lv_label_create(invRow); + lv_label_set_text(invTitle, "Pointer direction"); + lv_obj_set_flex_grow(invTitle, 1); + ctx->invertBtn = lv_btn_create(invRow); + lv_obj_add_event_cb(ctx->invertBtn, onInvertClicked, LV_EVENT_CLICKED, ctx); + lv_obj_t* invertLabelObj = lv_label_create(ctx->invertBtn); + lv_label_set_text(invertLabelObj, invertLabel(ctx->invertMode)); + lv_obj_set_style_text_font(invertLabelObj, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_obj_center(invertLabelObj); + + // Tilt-aware orientation + ctx->orientationSwitch = makeSettingsSwitch(ctx->content, "Tilt-aware orientation (use gravity)", ctx->useOrientation, onOrientationToggled, ctx); + + // Auto-clutch: freeze the pointer while the device is being repositioned. + // Default-on for devices with no touch screen, where the manual clutch + // (both click pads at once) can't be pressed. + ctx->autoClutchSwitch = makeSettingsSwitch(ctx->content, "Freeze pointer while repositioning", ctx->autoClutch, onAutoClutchToggled, ctx); + + // Re-calibrate + ctx->calibrateBtn = lv_btn_create(ctx->content); + lv_obj_t* calLabel = lv_label_create(ctx->calibrateBtn); + lv_label_set_text(calLabel, "Calibrate gyro"); + lv_obj_center(calLabel); + lv_obj_add_event_cb(ctx->calibrateBtn, onCalibrateClicked, LV_EVENT_CLICKED, ctx); + + lv_obj_t* note = lv_label_create(ctx->content); + lv_label_set_long_mode(note, LV_LABEL_LONG_MODE_WRAP); + lv_obj_set_width(note, LV_PCT(100)); + lv_label_set_text(note, "Settings are saved automatically."); + lv_obj_set_style_text_font(note, lvgl_get_text_font(FONT_SIZE_SMALL), 0); +} diff --git a/Apps/AirMouse/main/Source/main.cpp b/Apps/AirMouse/main/Source/main.cpp new file mode 100644 index 0000000..ad0d0f9 --- /dev/null +++ b/Apps/AirMouse/main/Source/main.cpp @@ -0,0 +1,67 @@ +#include "AirMouse.h" + +#include +#include +#include + +#include + +#include + +#include + +extern "C" { + +int main(int argc, char* argv[]) { + AppInstanceId app_instance_id = app_scheduler_current_app_id(); + + // Heap-allocated: BT event subscription/HID background work holds a raw Context* across the + // whole app instance lifetime, well past any single stack frame here. + auto ctx = std::make_unique(); + ctx->appInstanceId = app_instance_id; + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + ctx->eventGroup = &event_group; + + struct AppEventSubscription sub {}; + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + // Wakes the loop for the BT toggle requested by an LVGL event handler. + check(task_event_group_claim_bit(&event_group, &ctx->requestBit) == ERROR_NONE); + + // Must happen before window_manager_create() (which synchronously builds widgets) and before + // the wait loop below makes its first task_event_group_wait_any() call - see + // airMouseInitBt()'s doc comment. + airMouseInitBt(ctx.get()); + + WindowId window = window_manager_create(app_instance_id, airMouseCreateWidgets, ctx.get()); + ctx->window = window; + + bool should_close = false; + while (!should_close) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + struct AppEvent event; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + should_close = true; + } + if (should_close) break; + } + + airMouseProcessBtEvents(ctx.get()); + airMouseProcessRequests(ctx.get()); + } + + window_manager_remove(window); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + // Must unsubscribe from the BT event group (inside airMouseTeardown()) before destructing it + // below - releasing a subscription's bit needs the group to still be alive. + airMouseTeardown(ctx.get()); + task_event_group_release_bit(&event_group, ctx->requestBit); + task_event_group_destruct(&event_group); + + return 0; +} + +} diff --git a/Apps/AirMouse/manifest.properties b/Apps/AirMouse/manifest.properties new file mode 100644 index 0000000..36b04ac --- /dev/null +++ b/Apps/AirMouse/manifest.properties @@ -0,0 +1,11 @@ +manifest.version=0.3 +target.sdk=0.8.0-dev +target.platforms=esp32s3,esp32p4 +id=tactility.airmouse +version.name=0.4.0 +version.code=4 +name=Air Mouse +description=Bluetooth air mouse. Orientation-aware tilt control with auto calibration, adjustable sensitivity, a settings screen, on-screen left/right click buttons, and an optional auto-clutch that freezes the pointer while the device is being repositioned. +app.0.id=tactility.airmouse +app.0.name=Air Mouse +app.0.binary=airmouse diff --git a/Apps/Brainfuck/main/Source/Brainfuck.cpp b/Apps/Brainfuck/main/Source/Brainfuck.cpp index 20fb9f1..8a17af7 100644 --- a/Apps/Brainfuck/main/Source/Brainfuck.cpp +++ b/Apps/Brainfuck/main/Source/Brainfuck.cpp @@ -239,18 +239,18 @@ static void runCode(Context* ctx, const char* code) { static void showMainView(Context* ctx) { ctx->state = BfState::Main; - if (ctx->examplesList) lv_obj_add_flag(ctx->examplesList, LV_OBJ_FLAG_HIDDEN); - if (ctx->outputTa) lv_obj_remove_flag(ctx->outputTa, LV_OBJ_FLAG_HIDDEN); - if (ctx->inputRow) lv_obj_remove_flag(ctx->inputRow, LV_OBJ_FLAG_HIDDEN); - if (ctx->clrBtn) lv_obj_remove_flag(ctx->clrBtn, LV_OBJ_FLAG_HIDDEN); + if (ctx->examplesList) lv_obj_set_hidden(ctx->examplesList, true); + if (ctx->outputTa) lv_obj_set_hidden(ctx->outputTa, false); + if (ctx->inputRow) lv_obj_set_hidden(ctx->inputRow, false); + if (ctx->clrBtn) lv_obj_set_hidden(ctx->clrBtn, false); } static void showExamplesView(Context* ctx) { ctx->state = BfState::Examples; - if (ctx->outputTa) lv_obj_add_flag(ctx->outputTa, LV_OBJ_FLAG_HIDDEN); - if (ctx->inputRow) lv_obj_add_flag(ctx->inputRow, LV_OBJ_FLAG_HIDDEN); - if (ctx->examplesList) lv_obj_remove_flag(ctx->examplesList, LV_OBJ_FLAG_HIDDEN); - if (ctx->clrBtn) lv_obj_add_flag(ctx->clrBtn, LV_OBJ_FLAG_HIDDEN); + if (ctx->outputTa) lv_obj_set_hidden(ctx->outputTa, true); + if (ctx->inputRow) lv_obj_set_hidden(ctx->inputRow, true); + if (ctx->examplesList) lv_obj_set_hidden(ctx->examplesList, false); + if (ctx->clrBtn) lv_obj_set_hidden(ctx->clrBtn, true); } /* ── Callbacks ────────────────────────────────────────────────────── */ @@ -395,13 +395,13 @@ void brainfuckCreateWidgets(lv_obj_t* parent, void* userData) { g_ctx = ctx; ctx->state = BfState::Examples; - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Brainfuck interpreter"); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); ctx->clrBtn = lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, ctx); - lv_obj_add_flag(ctx->clrBtn, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->clrBtn, true); lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, ctx); lv_obj_t* cont = lv_obj_create(parent); @@ -410,7 +410,7 @@ void brainfuckCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(cont, 4, 0); lv_obj_set_style_pad_gap(cont, 4, 0); - lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(cont, false); lv_obj_set_style_border_width(cont, 0, 0); ctx->outputTa = lv_textarea_create(cont); @@ -420,7 +420,7 @@ void brainfuckCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_width(ctx->outputTa, LV_PCT(100)); lv_obj_set_flex_grow(ctx->outputTa, 1); lv_obj_set_style_text_font(ctx->outputTa, lv_font_get_default(), 0); - lv_obj_add_flag(ctx->outputTa, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->outputTa, true); ctx->inputRow = lv_obj_create(cont); lv_obj_set_size(ctx->inputRow, LV_PCT(100), LV_SIZE_CONTENT); @@ -428,9 +428,9 @@ void brainfuckCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_align(ctx->inputRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(ctx->inputRow, 0, 0); lv_obj_set_style_pad_gap(ctx->inputRow, 4, 0); - lv_obj_remove_flag(ctx->inputRow, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->inputRow, false); lv_obj_set_style_border_width(ctx->inputRow, 0, 0); - lv_obj_add_flag(ctx->inputRow, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->inputRow, true); ctx->inputTa = lv_textarea_create(ctx->inputRow); lv_textarea_set_placeholder_text(ctx->inputTa, "++++++[>++++++++++<-]>+++++."); diff --git a/Apps/Brainfuck/manifest.properties b/Apps/Brainfuck/manifest.properties index 11e9925..b8a82be 100644 --- a/Apps/Brainfuck/manifest.properties +++ b/Apps/Brainfuck/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.brainfuck -version.name=0.17.0 -version.code=17 +version.name=0.18.0 +version.code=18 name=Brainfuck description=Brainfuck esoteric language interpreter app.0.id=tactility.brainfuck diff --git a/Apps/Breakout/README.md b/Apps/Breakout/README.md index d60ada9..086ce08 100644 --- a/Apps/Breakout/README.md +++ b/Apps/Breakout/README.md @@ -44,7 +44,7 @@ Each level has a unique brick layout pattern. Colors rotate each level, and Silv [][][] [][][] [][][][][][] ``` -After level 12, layouts are **procedurally generated** using a seeded algorithm — the same level number always produces the same layout. +After level 12, layouts are **procedurally generated** using a seeded algorithm - the same level number always produces the same layout. ## Power-Up Capsules @@ -67,7 +67,7 @@ Capsules drop from destroyed bricks (~15% chance) and fall toward the paddle. Ca | Extend | `Blue` | **E** | Paddle width increases by 50% | | Catch | `Green` | **C** | Ball sticks to paddle on contact, auto-releases after 3 seconds | | Slow | `Orange` | **S** | Ball speed reduced to 60%, gradually recovers over 5 seconds | -| BreakOut | `Pink` | **B** | Opens exit on right wall — move paddle to exit for +10,000 pts | +| BreakOut | `Pink` | **B** | Opens exit on right wall - move paddle to exit for +10,000 pts | | Split | `Cyan` | **D** | Splits ball into 3! Life lost only when ALL balls are gone | | Extra Life | `Grey` | **+** | +1 life (rarest drop) | diff --git a/Apps/Breakout/main/Source/Breakout.cpp b/Apps/Breakout/main/Source/Breakout.cpp index a8729ff..37d595b 100644 --- a/Apps/Breakout/main/Source/Breakout.cpp +++ b/Apps/Breakout/main/Source/Breakout.cpp @@ -17,6 +17,7 @@ #include #include +#include constexpr auto* TAG = "Breakout"; @@ -164,7 +165,7 @@ static void onSoundToggled(lv_event_t* e); void breakoutCreateWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(parent, 0, 0); lv_obj_set_style_pad_row(parent, 0, 0); @@ -191,10 +192,10 @@ void breakoutCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_right(scoreWrap, 10, 0); lv_obj_set_style_border_width(scoreWrap, 0, 0); lv_obj_set_style_bg_opa(scoreWrap, 0, 0); - lv_obj_remove_flag(scoreWrap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(scoreWrap, false); ctx->scoreLabel = lv_label_create(scoreWrap); - lv_obj_set_style_text_font(ctx->scoreLabel, lv_font_get_default(), 0); + lv_obj_set_style_text_font(ctx->scoreLabel, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0); lv_obj_set_style_text_color(ctx->scoreLabel, lv_palette_main(LV_PALETTE_AMBER), 0); lv_obj_align(ctx->scoreLabel, LV_ALIGN_CENTER, 0, 0); @@ -207,10 +208,10 @@ void breakoutCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_right(livesWrap, 10, 0); lv_obj_set_style_border_width(livesWrap, 0, 0); lv_obj_set_style_bg_opa(livesWrap, 0, 0); - lv_obj_remove_flag(livesWrap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(livesWrap, false); ctx->livesLabel = lv_label_create(livesWrap); - lv_obj_set_style_text_font(ctx->livesLabel, lv_font_get_default(), 0); + lv_obj_set_style_text_font(ctx->livesLabel, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0); lv_obj_set_style_text_color(ctx->livesLabel, lv_palette_main(LV_PALETTE_RED), 0); lv_obj_align(ctx->livesLabel, LV_ALIGN_CENTER, 0, 0); @@ -264,8 +265,8 @@ void breakoutCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_border_width(ctx->gameArea, 0, 0); lv_obj_set_style_pad_all(ctx->gameArea, 0, 0); lv_obj_set_style_radius(ctx->gameArea, 0, 0); - lv_obj_remove_flag(ctx->gameArea, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_add_flag(ctx->gameArea, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_scrollable(ctx->gameArea, false); + lv_obj_set_clickable(ctx->gameArea, true); // Force layout to get accurate game area dimensions lv_obj_update_layout(parent); @@ -288,7 +289,7 @@ void breakoutCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_border_width(ctx->paddle, 0, 0); lv_obj_set_style_pad_all(ctx->paddle, 0, 0); lv_obj_set_style_radius(ctx->paddle, 2, 0); - lv_obj_remove_flag(ctx->paddle, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->paddle, false); // Create balls (primary + extras for split) for (int i = 0; i < MAX_BALLS; i++) { @@ -298,9 +299,9 @@ void breakoutCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_border_width(ctx->balls[i].obj, 0, 0); lv_obj_set_style_pad_all(ctx->balls[i].obj, 0, 0); lv_obj_set_style_radius(ctx->balls[i].obj, LV_RADIUS_CIRCLE, 0); - lv_obj_remove_flag(ctx->balls[i].obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->balls[i].obj, false); if (i > 0) { - lv_obj_add_flag(ctx->balls[i].obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->balls[i].obj, true); } } @@ -318,9 +319,9 @@ void breakoutCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_bg_opa(ctx->exitIndicator, LV_OPA_COVER, 0); lv_obj_set_style_border_width(ctx->exitIndicator, 0, 0); lv_obj_set_style_radius(ctx->exitIndicator, 0, 0); - lv_obj_remove_flag(ctx->exitIndicator, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->exitIndicator, false); lv_obj_set_pos(ctx->exitIndicator, ctx->areaW - 6, ctx->paddleYPos - exitH / 2 + ctx->paddleH / 2); - lv_obj_add_flag(ctx->exitIndicator, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->exitIndicator, true); // Message overlay (centered in game area) ctx->messageLabel = lv_label_create(ctx->gameArea); @@ -341,18 +342,18 @@ void breakoutCreateWidgets(lv_obj_t* parent, void* userData) { for (int i = 0; i < MAX_BALLS; i++) { if (ctx->balls[i].active && ctx->balls[i].obj) { lv_obj_set_pos(ctx->balls[i].obj, (int)ctx->balls[i].x, (int)ctx->balls[i].y); - lv_obj_clear_flag(ctx->balls[i].obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->balls[i].obj, false); } } // Restore falling capsules for (int i = 0; i < MAX_CAPSULES; i++) { if (ctx->capsules[i].active && ctx->capsuleObjs[i]) { lv_obj_set_pos(ctx->capsuleObjs[i], (int)ctx->capsules[i].x, (int)ctx->capsules[i].y); - lv_obj_clear_flag(ctx->capsuleObjs[i], LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->capsuleObjs[i], false); } } // Restore exit indicator - if (ctx->exitOpen && ctx->exitIndicator) lv_obj_clear_flag(ctx->exitIndicator, LV_OBJ_FLAG_HIDDEN); + if (ctx->exitOpen && ctx->exitIndicator) lv_obj_set_hidden(ctx->exitIndicator, false); updateScoreDisplay(ctx); updateMessage(ctx); } @@ -421,9 +422,9 @@ static void createCapsuleObjs(Context* ctx) { lv_obj_set_style_border_color(ctx->capsuleObjs[i], lv_color_white(), 0); lv_obj_set_style_pad_all(ctx->capsuleObjs[i], 0, 0); lv_obj_set_style_radius(ctx->capsuleObjs[i], 3, 0); - lv_obj_remove_flag(ctx->capsuleObjs[i], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_remove_flag(ctx->capsuleObjs[i], LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_flag(ctx->capsuleObjs[i], LV_OBJ_FLAG_HIDDEN); + lv_obj_set_scrollable(ctx->capsuleObjs[i], false); + lv_obj_set_clickable(ctx->capsuleObjs[i], false); + lv_obj_set_hidden(ctx->capsuleObjs[i], true); ctx->capsuleLabels[i] = lv_label_create(ctx->capsuleObjs[i]); lv_obj_set_style_text_color(ctx->capsuleLabels[i], lv_color_white(), 0); @@ -440,8 +441,8 @@ static void createLaserObjs(Context* ctx) { lv_obj_set_style_border_width(ctx->lasers[i].obj, 0, 0); lv_obj_set_style_pad_all(ctx->lasers[i].obj, 0, 0); lv_obj_set_style_radius(ctx->lasers[i].obj, 0, 0); - lv_obj_remove_flag(ctx->lasers[i].obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_add_flag(ctx->lasers[i].obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_scrollable(ctx->lasers[i].obj, false); + lv_obj_set_hidden(ctx->lasers[i].obj, true); ctx->lasers[i].active = false; } } @@ -458,8 +459,8 @@ static void createBricks(Context* ctx) { lv_obj_set_style_border_width(ctx->bricks[idx], 0, 0); lv_obj_set_style_pad_all(ctx->bricks[idx], 0, 0); lv_obj_set_style_radius(ctx->bricks[idx], 2, 0); - lv_obj_remove_flag(ctx->bricks[idx], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_remove_flag(ctx->bricks[idx], LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_scrollable(ctx->bricks[idx], false); + lv_obj_set_clickable(ctx->bricks[idx], false); int x = ctx->brickOffsetX + c * (ctx->brickW + ctx->brickGap); int y = ctx->brickOffsetY + r * (ctx->brickH + ctx->brickGap); @@ -642,11 +643,11 @@ static void refreshBricks(Context* ctx) { if (!ctx->bricks[idx]) continue; if (!ctx->brickAlive[idx]) { - lv_obj_add_flag(ctx->bricks[idx], LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->bricks[idx], true); continue; } - lv_obj_clear_flag(ctx->bricks[idx], LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->bricks[idx], false); switch (ctx->brickType[idx]) { case BrickType::Normal: { @@ -701,7 +702,7 @@ static void hitBrick(Context* ctx, int idx) { if (ctx->brickHits[idx] <= 0) { // Brick destroyed ctx->brickAlive[idx] = false; - if (ctx->bricks[idx]) lv_obj_add_flag(ctx->bricks[idx], LV_OBJ_FLAG_HIDDEN); + if (ctx->bricks[idx]) lv_obj_set_hidden(ctx->bricks[idx], true); ctx->score += scoreBrick(ctx, idx); if (ctx->brickType[idx] != BrickType::Gold) { ctx->bricksRemaining--; @@ -798,7 +799,7 @@ static void resetBall(Context* ctx) { // Reset to single ball for (int i = 1; i < MAX_BALLS; i++) { ctx->balls[i].active = false; - if (ctx->balls[i].obj) lv_obj_add_flag(ctx->balls[i].obj, LV_OBJ_FLAG_HIDDEN); + if (ctx->balls[i].obj) lv_obj_set_hidden(ctx->balls[i].obj, true); } ctx->activeBallCount = 1; @@ -809,7 +810,7 @@ static void resetBall(Context* ctx) { ctx->balls[0].vy = 0; if (ctx->balls[0].obj) { lv_obj_set_pos(ctx->balls[0].obj, (int)ctx->balls[0].x, (int)ctx->balls[0].y); - lv_obj_clear_flag(ctx->balls[0].obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->balls[0].obj, false); } } @@ -905,7 +906,7 @@ static void spawnCapsule(Context* ctx, float x, float y) { if (ctx->capsuleObjs[i]) { lv_obj_set_style_bg_color(ctx->capsuleObjs[i], lv_color_hex(CAPSULE_COLORS[typeIdx]), 0); lv_obj_set_pos(ctx->capsuleObjs[i], (int)x, (int)y); - lv_obj_clear_flag(ctx->capsuleObjs[i], LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->capsuleObjs[i], false); } if (ctx->capsuleLabels[i]) { lv_label_set_text(ctx->capsuleLabels[i], CAPSULE_LETTERS[typeIdx]); @@ -924,7 +925,7 @@ static void updateCapsules(Context* ctx) { // Off screen if (ctx->capsules[i].y > ctx->areaH) { ctx->capsules[i].active = false; - if (ctx->capsuleObjs[i]) lv_obj_add_flag(ctx->capsuleObjs[i], LV_OBJ_FLAG_HIDDEN); + if (ctx->capsuleObjs[i]) lv_obj_set_hidden(ctx->capsuleObjs[i], true); continue; } @@ -935,7 +936,7 @@ static void updateCapsules(Context* ctx) { ctx->capsules[i].x < ctx->paddleX + ctx->paddleW) { activatePowerUp(ctx, ctx->capsules[i].type); ctx->capsules[i].active = false; - if (ctx->capsuleObjs[i]) lv_obj_add_flag(ctx->capsuleObjs[i], LV_OBJ_FLAG_HIDDEN); + if (ctx->capsuleObjs[i]) lv_obj_set_hidden(ctx->capsuleObjs[i], true); continue; } @@ -1030,13 +1031,13 @@ static void clearPowerUps(Context* ctx) { ctx->laserCooldown = 0; for (int i = 0; i < MAX_LASERS; i++) { ctx->lasers[i].active = false; - if (ctx->lasers[i].obj) lv_obj_add_flag(ctx->lasers[i].obj, LV_OBJ_FLAG_HIDDEN); + if (ctx->lasers[i].obj) lv_obj_set_hidden(ctx->lasers[i].obj, true); } // Clear capsules for (int i = 0; i < MAX_CAPSULES; i++) { ctx->capsules[i].active = false; - if (ctx->capsuleObjs[i]) lv_obj_add_flag(ctx->capsuleObjs[i], LV_OBJ_FLAG_HIDDEN); + if (ctx->capsuleObjs[i]) lv_obj_set_hidden(ctx->capsuleObjs[i], true); } // Close exit @@ -1071,7 +1072,7 @@ static void splitBalls(Context* ctx) { if (ctx->balls[i].obj) { lv_obj_set_pos(ctx->balls[i].obj, (int)ctx->balls[i].x, (int)ctx->balls[i].y); - lv_obj_clear_flag(ctx->balls[i].obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->balls[i].obj, false); } spawned++; } @@ -1127,7 +1128,7 @@ static void updateBalls(Context* ctx) { // Bottom edge if (ctx->balls[b].y + ctx->ballSize > ctx->areaH) { ctx->balls[b].active = false; - if (ctx->balls[b].obj) lv_obj_add_flag(ctx->balls[b].obj, LV_OBJ_FLAG_HIDDEN); + if (ctx->balls[b].obj) lv_obj_set_hidden(ctx->balls[b].obj, true); ctx->activeBallCount--; if (ctx->activeBallCount <= 0) { ctx->activeBallCount = 0; @@ -1215,7 +1216,7 @@ static void fireLaser(Context* ctx) { ctx->lasers[i].y = (float)(ctx->paddleYPos - ctx->laserH); if (ctx->lasers[i].obj) { lv_obj_set_pos(ctx->lasers[i].obj, (int)ctx->lasers[i].x, (int)ctx->lasers[i].y); - lv_obj_clear_flag(ctx->lasers[i].obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->lasers[i].obj, false); } if (ctx->sfxEngine) ctx->sfxEngine->play(SfxId::Laser); return; @@ -1240,7 +1241,7 @@ static void updateLasers(Context* ctx) { if (ctx->lasers[i].y + ctx->laserH < 0) { ctx->lasers[i].active = false; - if (ctx->lasers[i].obj) lv_obj_add_flag(ctx->lasers[i].obj, LV_OBJ_FLAG_HIDDEN); + if (ctx->lasers[i].obj) lv_obj_set_hidden(ctx->lasers[i].obj, true); continue; } @@ -1269,7 +1270,7 @@ static void checkLaserBrickCollisions(Context* ctx) { ctx->lasers[li].y + ctx->laserH > by && ctx->lasers[li].y < by + ctx->brickH) { hitBrick(ctx, idx); ctx->lasers[li].active = false; - if (ctx->lasers[li].obj) lv_obj_add_flag(ctx->lasers[li].obj, LV_OBJ_FLAG_HIDDEN); + if (ctx->lasers[li].obj) lv_obj_set_hidden(ctx->lasers[li].obj, true); goto nextLaser; } } @@ -1282,12 +1283,12 @@ static void checkLaserBrickCollisions(Context* ctx) { static void openExit(Context* ctx) { ctx->exitOpen = true; - if (ctx->exitIndicator) lv_obj_clear_flag(ctx->exitIndicator, LV_OBJ_FLAG_HIDDEN); + if (ctx->exitIndicator) lv_obj_set_hidden(ctx->exitIndicator, false); } static void closeExit(Context* ctx) { ctx->exitOpen = false; - if (ctx->exitIndicator) lv_obj_add_flag(ctx->exitIndicator, LV_OBJ_FLAG_HIDDEN); + if (ctx->exitIndicator) lv_obj_set_hidden(ctx->exitIndicator, true); } /* ── Main Game Tick ───────────────────────────────────────────── */ @@ -1369,15 +1370,15 @@ static void updateMessage(Context* ctx) { snprintf(buf, sizeof(buf), "%s to start!", input_hint); } lv_label_set_text(ctx->messageLabel, buf); - lv_obj_clear_flag(ctx->messageLabel, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->messageLabel, false); break; } case GameState::Playing: - lv_obj_add_flag(ctx->messageLabel, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->messageLabel, true); break; case GameState::Paused: lv_label_set_text(ctx->messageLabel, "PAUSED"); - lv_obj_clear_flag(ctx->messageLabel, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->messageLabel, false); break; case GameState::GameOver: { char buf[64]; @@ -1387,7 +1388,7 @@ static void updateMessage(Context* ctx) { snprintf(buf, sizeof(buf), "Game Over\nScore: %d\nBest Score: %d", ctx->score, (int)highScore); } lv_label_set_text(ctx->messageLabel, buf); - lv_obj_clear_flag(ctx->messageLabel, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->messageLabel, false); break; } } diff --git a/Apps/Breakout/manifest.properties b/Apps/Breakout/manifest.properties index 9f510fd..bd129aa 100644 --- a/Apps/Breakout/manifest.properties +++ b/Apps/Breakout/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.breakout -version.name=0.21.0 -version.code=21 +version.name=0.22.0 +version.code=22 name=Breakout description=Classic brick-breaking arcade game app.0.id=tactility.breakout diff --git a/Apps/Calculator/main/Source/Calculator.cpp b/Apps/Calculator/main/Source/Calculator.cpp index 08cb4d0..9dfc529 100644 --- a/Apps/Calculator/main/Source/Calculator.cpp +++ b/Apps/Calculator/main/Source/Calculator.cpp @@ -206,7 +206,7 @@ static void onButtonPressed(lv_event_t* e) { void calculatorCreateWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); @@ -225,7 +225,7 @@ void calculatorCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_right(wrapper, 10, LV_PART_MAIN); lv_obj_set_style_pad_column(wrapper, 40, LV_PART_MAIN); lv_obj_set_style_border_width(wrapper, 0, 0); - lv_obj_remove_flag(wrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(wrapper, false); ctx->displayLabel = lv_label_create(wrapper); lv_label_set_text(ctx->displayLabel, "0"); diff --git a/Apps/Calculator/manifest.properties b/Apps/Calculator/manifest.properties index 04c097f..03c1cfd 100644 --- a/Apps/Calculator/manifest.properties +++ b/Apps/Calculator/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.calculator -version.name=0.18.0 -version.code=18 +version.name=0.19.0 +version.code=19 name=Calculator app.0.id=tactility.calculator app.0.name=Calculator diff --git a/Apps/Diceware/main/Source/Diceware.cpp b/Apps/Diceware/main/Source/Diceware.cpp index 8f4c00c..483e83c 100644 --- a/Apps/Diceware/main/Source/Diceware.cpp +++ b/Apps/Diceware/main/Source/Diceware.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -115,7 +116,7 @@ static void onHelpClicked(lv_event_t* e) { "The hardware random number generator can use the Wi-Fi radio to improve randomness. There's no need to connect to a Wi-Fi network for this to work.", "OK", }; - app_start_for_result("AlertDialog", 3, argv, ctx->appInstanceId, &ctx->pendingHelpDialogId); + app_start_for_result("tactility.alertdialog", 3, argv, ctx->appInstanceId, &ctx->pendingHelpDialogId); } void dicewareCreateWidgets(lv_obj_t* parent, void* userData) { @@ -130,43 +131,52 @@ void dicewareCreateWidgets(lv_obj_t* parent, void* userData) { auto* wrapper = lv_obj_create(parent); lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); - auto* top_row_wrapper = lv_obj_create(wrapper); - lv_obj_set_style_border_width(top_row_wrapper, 0, LV_STATE_DEFAULT); + auto* top_row_wrapper = lvgl_card_create(wrapper); lv_obj_set_size(top_row_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(top_row_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(top_row_wrapper, LV_FLEX_FLOW_COLUMN); - auto* generate_button = lv_button_create(top_row_wrapper); - lv_obj_align(generate_button, LV_ALIGN_LEFT_MID, 0, 0); + auto* controls_row = lv_obj_create(top_row_wrapper); + lv_obj_remove_style_all(controls_row); + lv_obj_set_size(controls_row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(controls_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(controls_row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_scrollable(controls_row, false); + + auto* generate_button = lv_button_create(controls_row); auto* generate_label = lv_label_create(generate_button); lv_label_set_text(generate_label, "Generate"); lv_obj_add_event_cb(generate_button, onClickGenerate, LV_EVENT_SHORT_CLICKED, ctx); - ctx->spinbox = lv_spinbox_create(top_row_wrapper); + auto* spinbox_controls = lv_obj_create(controls_row); + lv_obj_remove_style_all(spinbox_controls); + lv_obj_set_size(spinbox_controls, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(spinbox_controls, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(spinbox_controls, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(spinbox_controls, 8, LV_STATE_DEFAULT); + lv_obj_set_scrollable(spinbox_controls, false); + + auto* spinbox_dec_button = lv_button_create(spinbox_controls); + auto* spinbox_dec_label = lv_label_create(spinbox_dec_button); + lv_label_set_text(spinbox_dec_label, LV_SYMBOL_MINUS); + lv_obj_center(spinbox_dec_label); + + ctx->spinbox = lv_spinbox_create(spinbox_controls); lv_spinbox_set_range(ctx->spinbox, 4, 12); lv_spinbox_set_value(ctx->spinbox, 5); lv_spinbox_set_step(ctx->spinbox, 1); lv_spinbox_set_digit_format(ctx->spinbox, 1, 0); - lv_obj_set_width(ctx->spinbox, 30); - - auto* spinbox_dec_button = lv_button_create(top_row_wrapper); - lv_obj_set_style_bg_image_src(spinbox_dec_button, LV_SYMBOL_MINUS, LV_STATE_DEFAULT); + lv_obj_set_width(ctx->spinbox, LV_SIZE_CONTENT); + lv_obj_set_style_text_align(ctx->spinbox, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); lv_obj_add_event_cb(spinbox_dec_button, onSpinboxDecrement, LV_EVENT_SHORT_CLICKED, ctx->spinbox); - lv_obj_set_style_pad_all(spinbox_dec_button, 16, LV_STATE_DEFAULT); - auto* spinbox_inc_button = lv_button_create(top_row_wrapper); - // lv_obj_align_to(spinbox_inc_button, spinbox, LV_ALIGN_OUT_RIGHT_MID, 5, 0); - lv_obj_set_style_bg_image_src(spinbox_inc_button, LV_SYMBOL_PLUS, LV_STATE_DEFAULT); + auto* spinbox_inc_button = lv_button_create(spinbox_controls); + auto* spinbox_inc_label = lv_label_create(spinbox_inc_button); + lv_label_set_text(spinbox_inc_label, LV_SYMBOL_PLUS); + lv_obj_center(spinbox_inc_label); lv_obj_add_event_cb(spinbox_inc_button, onSpinboxIncrement, LV_EVENT_SHORT_CLICKED, ctx->spinbox); - lv_obj_set_style_pad_all(spinbox_inc_button, 16, LV_STATE_DEFAULT); - - // Align spinbox widgets - lv_obj_align(spinbox_inc_button, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_align_to(ctx->spinbox, spinbox_inc_button, LV_ALIGN_OUT_LEFT_MID, -5, 0); - lv_obj_align_to(spinbox_dec_button, ctx->spinbox, LV_ALIGN_OUT_LEFT_MID, -5, 0); auto* result_wrapper = lv_obj_create(wrapper); lv_obj_set_flex_grow(result_wrapper, 1); diff --git a/Apps/Diceware/manifest.properties b/Apps/Diceware/manifest.properties index 2ad0638..2d05f6e 100644 --- a/Apps/Diceware/manifest.properties +++ b/Apps/Diceware/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.diceware -version.name=0.20.0 -version.code=20 +version.name=0.21.0 +version.code=21 name=Diceware description=Password generator app.0.id=tactility.diceware diff --git a/Apps/Doom/main/Source/DoomAudio.cpp b/Apps/Doom/main/Source/DoomAudio.cpp index c54a85d..28dc9d0 100644 --- a/Apps/Doom/main/Source/DoomAudio.cpp +++ b/Apps/Doom/main/Source/DoomAudio.cpp @@ -144,6 +144,20 @@ void mixerTask(void* /*arg*/) { uint32_t yieldCounter = 0; while (!stopRequested.load()) { + if (outputHandle == nullptr) { + // Output died (hotplug) and the last reopen failed: retry before + // rendering anything. Rendering first would advance the MIDI queue + // and consume SFX channel positions for audio nobody will ever + // hear - and the publish-back below would then throw that progress + // away. The back-off keeps the pace identical to the post-failure + // path rather than spinning the reopen call every iteration. + error_t reopenError = audio_stream_open_output(streamDevice, &config, &outputHandle); + if (reopenError != ERROR_NONE) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + continue; + } + // Music forms the base layer: DG_OPL_Render() fills the whole buffer // (writing silence when nothing is playing), so no memset is needed. // It also advances the MIDI timer queue by exactly MIXBUFFER_SAMPLES @@ -238,11 +252,21 @@ void mixerTask(void* /*arg*/) { error_t writeError = audio_stream_write(outputHandle, outBuffer, sizeof(outBuffer), &bytesWritten, pdMS_TO_TICKS(500)); if (writeError != ERROR_NONE) { + // ERROR_INVALID_STATE/ERROR_RESOURCE here typically mean a hotplug codec change + // (e.g. USB audio attach/detach) re-bound the output direction to a different + // codec device - the old handle is now permanently dead. Close and reopen against + // whatever codec is current instead of retrying the same dead handle forever. + ESP_LOGW(TAG, "Mixer write failed (%s) - reopening", error_to_string(writeError)); + audio_stream_close(outputHandle); + outputHandle = nullptr; + error_t reopenError = audio_stream_open_output(streamDevice, &config, &outputHandle); + if (reopenError != ERROR_NONE) { + ESP_LOGE(TAG, "Failed to reopen output stream: %s", error_to_string(reopenError)); + } // A failing write returns immediately rather than blocking for the // buffer to drain. Without a delay here this loop spins flat out at // near-top priority and starves the idle task on this core, which // trips the task watchdog. Back off so the system stays alive. - ESP_LOGW(TAG, "Mixer write failed: %s", error_to_string(writeError)); vTaskDelay(pdMS_TO_TICKS(10)); } else if (bytesWritten < sizeof(outBuffer)) { // Short write: the stream accepted the data without blocking, so @@ -261,7 +285,9 @@ void mixerTask(void* /*arg*/) { } - audio_stream_close(outputHandle); + if (outputHandle) { + audio_stream_close(outputHandle); + } xSemaphoreGive(stoppedSem); vTaskDelete(nullptr); } diff --git a/Apps/Doom/main/doomgeneric/doomgeneric/doomtype.h b/Apps/Doom/main/doomgeneric/doomgeneric/doomtype.h index 9b2bd52..a5a5ac8 100644 --- a/Apps/Doom/main/doomgeneric/doomgeneric/doomtype.h +++ b/Apps/Doom/main/doomgeneric/doomgeneric/doomtype.h @@ -92,8 +92,10 @@ typedef enum #else // C23 makes false/true reserved keywords, so this enum can no longer declare them as -// enumerator names. C23 already has a real bool, so alias boolean to it directly. -typedef bool boolean; +// enumerator names. Keep boolean 4 bytes wide like upstream Doom: legacy code casts +// boolean* to int* (e.g. st_stuff.c arms widget -> player_t.weaponowned[]), which +// breaks if boolean is only 1 byte. +typedef uint32_t boolean; #define undef 0xFFFFFFFF #endif diff --git a/Apps/EpubReader/main/Source/EpubReader.cpp b/Apps/EpubReader/main/Source/EpubReader.cpp index bbce5a0..adc3724 100644 --- a/Apps/EpubReader/main/Source/EpubReader.cpp +++ b/Apps/EpubReader/main/Source/EpubReader.cpp @@ -217,7 +217,7 @@ void epubReaderCreateWidgets(lv_obj_t* parent, void* userData) { "OK", }; app_start_for_result( - "AlertDialog", 3, argv, ctx->appInstanceId, &ctx->psramAlertId_ + "tactility.alertdialog", 3, argv, ctx->appInstanceId, &ctx->psramAlertId_ ); } return; @@ -240,7 +240,7 @@ void epubReaderCreateWidgets(lv_obj_t* parent, void* userData) { } lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); ctx->toolbar_ = lvgl_toolbar_create(parent, "Epub Reader"); @@ -251,7 +251,7 @@ void epubReaderCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_all(ctx->wrapperWidget_, 0, 0); lv_obj_set_style_border_width(ctx->wrapperWidget_, 0, 0); lv_obj_set_style_bg_opa(ctx->wrapperWidget_, LV_OPA_TRANSP, 0); - lv_obj_remove_flag(ctx->wrapperWidget_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->wrapperWidget_, false); // Attempt to open a book: launch parameter → saved progress (first show only). // Both paths use lv_async_call so EpubService::open runs at fresh stack diff --git a/Apps/EpubReader/main/Source/EpubReaderUI.cpp b/Apps/EpubReader/main/Source/EpubReaderUI.cpp index 80123cc..9cc9bf8 100644 --- a/Apps/EpubReader/main/Source/EpubReaderUI.cpp +++ b/Apps/EpubReader/main/Source/EpubReaderUI.cpp @@ -175,8 +175,8 @@ void buildReaderUI(Context* ctx, lv_obj_t* parent) { (lv_coord_t)lv_font_get_line_height(selectContentFont(false, false)), 0); lv_obj_set_style_border_width(ctx->contentWidget_, 0, 0); lv_obj_set_style_bg_opa(ctx->contentWidget_, LV_OPA_TRANSP, 0); - lv_obj_remove_flag(ctx->contentWidget_, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_remove_flag(ctx->contentWidget_, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_scrollable(ctx->contentWidget_, false); + lv_obj_set_clickable(ctx->contentWidget_, false); loadChapter(ctx, ctx->currentSpineIndex_, +1); } } @@ -206,7 +206,7 @@ static void renderSpanParagraph(lv_obj_t* parent, const char* seg, size_t len, lv_obj_set_style_pad_all(sg, 0, 0); // keep heights = N*lineH (same rule as lv_label) lv_spangroup_set_align(sg, align); lv_spangroup_set_mode(sg, LV_SPAN_MODE_BREAK); // wrap to content height - lv_obj_remove_flag(sg, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_clickable(sg, false); bool bold = false, italic = false; std::string run; @@ -314,7 +314,7 @@ void buildBrowserUI(Context* ctx, lv_obj_t* parent) { lv_obj_set_style_pad_gap(pathBar, 4, 0); lv_obj_set_style_border_width(pathBar, 0, 0); lv_obj_set_style_bg_opa(pathBar, LV_OPA_TRANSP, 0); - lv_obj_remove_flag(pathBar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(pathBar, false); if (ctx->browsePath_ != ctx->dataRoot_) { lv_obj_t* backBtn = lv_button_create(pathBar); @@ -433,7 +433,7 @@ void buildShelfUI(Context* ctx, lv_obj_t* parent) { lv_obj_set_style_pad_gap(pathBar, padBar, 0); lv_obj_set_style_border_width(pathBar, 0, 0); lv_obj_set_style_bg_opa(pathBar, LV_OPA_TRANSP, 0); - lv_obj_remove_flag(pathBar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(pathBar, false); lv_obj_t* backBtn = lv_button_create(pathBar); lv_obj_set_size(backBtn, LV_SIZE_CONTENT, LV_SIZE_CONTENT); @@ -477,7 +477,7 @@ void buildShelfUI(Context* ctx, lv_obj_t* parent) { lv_obj_set_width(list, LV_PCT(100)); lv_obj_set_flex_grow(list, 1); lv_obj_set_style_border_width(list, 0, 0); - lv_obj_remove_flag(list, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(list, false); if (ctx->browserEntries_.empty()) { lv_list_add_text(list, "No books found in books folder."); @@ -513,7 +513,7 @@ void buildShelfUI(Context* ctx, lv_obj_t* parent) { lv_obj_set_style_pad_gap(navBar, padBar, 0); lv_obj_set_style_border_width(navBar, 0, 0); lv_obj_set_style_bg_opa(navBar, LV_OPA_TRANSP, 0); - lv_obj_remove_flag(navBar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(navBar, false); bool canPrev = (ctx->shelfPage_ > 0); bool canNext = (ctx->shelfPage_ < totalPages - 1); diff --git a/Apps/EpubReader/manifest.properties b/Apps/EpubReader/manifest.properties index 76d54e0..d3bf9f6 100644 --- a/Apps/EpubReader/manifest.properties +++ b/Apps/EpubReader/manifest.properties @@ -2,10 +2,11 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32s3,esp32p4 id=tactility.epubreader -version.name=0.17.0 -version.code=17 +version.name=0.18.0 +version.code=18 +requires.ram=2M name=ePub Reader -description=ePub and text file reader. Requires PSRAM! +description=ePub and text file reader. app.0.id=tactility.epubreader app.0.name=ePub Reader app.0.binary=epubreader diff --git a/Apps/EspNowBridge/main/Source/EspNowBridge.cpp b/Apps/EspNowBridge/main/Source/EspNowBridge.cpp index e50a186..8159ccd 100644 --- a/Apps/EspNowBridge/main/Source/EspNowBridge.cpp +++ b/Apps/EspNowBridge/main/Source/EspNowBridge.cpp @@ -240,10 +240,10 @@ static void setUpdateButtonsDisabled(Context* ctx, bool disabled) { static void refreshWifiPrompt(Context* ctx) { if (isWifiRadioOn(ctx)) { - lv_obj_add_flag(ctx->enableWifiButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->enableWifiButton, true); setUpdateButtonsDisabled(ctx, false); } else { - lv_obj_clear_flag(ctx->enableWifiButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->enableWifiButton, false); setUpdateButtonsDisabled(ctx, true); } } @@ -684,7 +684,7 @@ void espNowBridgeCreateWidgets(lv_obj_t* parent, void* userData) { ctx->isShown = false; - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "ESP-NOW Bridge"); diff --git a/Apps/EspNowBridge/manifest.properties b/Apps/EspNowBridge/manifest.properties index d731aed..6d8144f 100644 --- a/Apps/EspNowBridge/manifest.properties +++ b/Apps/EspNowBridge/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32p4 id=tactility.espnowbridge -version.name=0.15.0 -version.code=15 +version.name=0.16.0 +version.code=16 name=ESP-NOW Bridge description=Companion app for updating P4 device C6 co-processor firmware to enable ESP-NOW bridge support. app.0.id=tactility.espnowbridge diff --git a/Apps/GfxBenchmark/CMakeLists.txt b/Apps/GfxBenchmark/CMakeLists.txt new file mode 100644 index 0000000..be6c355 --- /dev/null +++ b/Apps/GfxBenchmark/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(gfxbenchmark) +project(gfxbenchmark) +tactility_project_post(gfxbenchmark) diff --git a/Apps/GfxBenchmark/README.md b/Apps/GfxBenchmark/README.md new file mode 100644 index 0000000..086ed6d --- /dev/null +++ b/Apps/GfxBenchmark/README.md @@ -0,0 +1,70 @@ +# Gfx Benchmark + +A PDQ-style primitive benchmark for the direct-to-display `Libraries/Gfx` stack. It is modelled on +the LCD GFX test in `M5UnitTest`, but draws through `gfx::Target` so it runs on any display the +Gfx library supports: MIPI DSI, RGB, SPI, e-paper-class mono/grayscale panels, hardware frame +buffers, full-frame buffers, and banded rendering. + +## Phases + +The app runs the same phases as the LCD GFX test, plus two extra phases for the image and polygon +paths, adapting sizes and step counts to the display so a 135x240 panel and a 720x1280 panel both +finish in a reasonable time: + +1. Screen fill +2. Text (5x7, proportional 5x7, 8x8, 8x16, scaling, wrapping) +3. Pixels +4. Lines (four corners, two sweeps each) +5. H/V lines +6. Rectangles (filled) +7. Rectangles (outline) +8. Triangles (filled) +9. Triangles (outline) +10. Circles (filled) +11. Circles (outline) +12. Arcs (filled) +13. Arcs (outline) +14. Round rects (filled) +15. Round rects (outline) +16. Sprites (Canvas, colour-key blit, alpha mask) +17. Polygons (filled star, outline pentagon) + +Every phase starts with a clear and runs with `Prepare::Overwrite`, so the benchmark is not +affected by leftovers in a flip back buffer. After each phase has been measured, it is held on +screen for 250 ms so it is visible; that delay is not included in the results. + +## Results + +After the last phase the app draws an on-screen table: + +- Phase name and draw time in microseconds, right-aligned. +- Total draw and present time in the footer. +- Render mode and resolution in the subtitle. +- Short landscape panels (Cardputer, T-Embed) switch to a two-column table, so all phases and the + full footer fit. +- If the table still doesn't fit, it shows the slowest phases that do. +- On very narrow displays (under 120 px) a compact summary shows the total and the slowest phase. + +The same numbers are logged to the serial console with draw, present, total and dirty-pixel counts. +This makes it easy to compare render modes and panels without reading the display. + +## Controls + +- Any input cancels the run or exits the results screen. +- The app also honours `APP_EVENT_CLOSE`, so the app manager can stop it normally. + +## Build and run + +```sh +tt app build --path Apps/GfxBenchmark --architecture esp32s3 +tt app bir --path Apps/GfxBenchmark --architecture esp32s3 --host +``` + +Replace the architecture with `esp32`, `esp32c6` or `esp32p4` as needed. The app targets every +platform in its manifest. + +## Slow-refresh displays + +E-paper and other slow-refresh panels skip the benchmark and show a message instead. Running the +full PDQ workload on a panel that needs seconds per refresh would not produce meaningful numbers. +The message is drawn with the same Gfx text path and followed by a full-quality refresh. diff --git a/Apps/GfxBenchmark/main/CMakeLists.txt b/Apps/GfxBenchmark/main/CMakeLists.txt new file mode 100644 index 0000000..d64a8cc --- /dev/null +++ b/Apps/GfxBenchmark/main/CMakeLists.txt @@ -0,0 +1,11 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +file(GLOB_RECURSE GFX_FILES ../../../Libraries/Gfx/Source/*.c*) +file(GLOB_RECURSE INPUT_FILES ../../../Libraries/Input/Source/*.c*) + +tactility_component_register( + SRCS ${SOURCE_FILES} ${GFX_FILES} ${INPUT_FILES} + INCLUDE_DIRS "Include" ../../../Libraries/Gfx/Include ../../../Libraries/Input/Include + REQUIRES graphics-module esp_driver_ppa +) diff --git a/Apps/GfxBenchmark/main/Include/Application.h b/Apps/GfxBenchmark/main/Include/Application.h new file mode 100644 index 0000000..a84e34c --- /dev/null +++ b/Apps/GfxBenchmark/main/Include/Application.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +void runApplication(gfx::Target* target, AnyInput* input); diff --git a/Apps/GfxBenchmark/main/Source/Application.cpp b/Apps/GfxBenchmark/main/Source/Application.cpp new file mode 100644 index 0000000..6a679c9 --- /dev/null +++ b/Apps/GfxBenchmark/main/Source/Application.cpp @@ -0,0 +1,847 @@ +#include "Application.h" + +#include + +#include +#include + +#include + +#include +#include +#include +#include + +constexpr auto TAG = "GfxBenchmark"; + +/** How long each phase stays on screen after it has been measured. Not part of the timing. */ +constexpr uint32_t PHASE_PREVIEW_MS = 250; + +namespace { + +constexpr uint16_t BLACK = 0x0000; +constexpr uint16_t WHITE = 0xFFFF; +constexpr uint16_t RED = 0xF800; +constexpr uint16_t GREEN = 0x07E0; +constexpr uint16_t BLUE = 0x001F; +constexpr uint16_t YELLOW = 0xFFE0; +constexpr uint16_t CYAN = 0x07FF; +constexpr uint16_t MAGENTA = 0xF81F; +constexpr uint16_t ORANGE = 0xFD20; +constexpr uint16_t GREY = 0x8410; +constexpr uint16_t DARK_STRIPE = 0x0841; + +constexpr int PHASE_COUNT = 17; + +static const char* const PHASE_NAMES[PHASE_COUNT] = { + "Screen fill", + "Text", + "Pixels", + "Lines", + "H/V lines", + "Rectangles (filled)", + "Rectangles (outline)", + "Triangles (filled)", + "Triangles (outline)", + "Circles (filled)", + "Circles (outline)", + "Arcs (filled)", + "Arcs (outline)", + "Round rects (filled)", + "Round rects (outline)", + "Sprites", + "Polygons", +}; + +// Short enough for the results table on a 128 px wide display. +static const char* const PHASE_SHORT_NAMES[PHASE_COUNT] = { + "Fill", + "Text", + "Pixels", + "Lines", + "H/V", + "RectF", + "Rect", + "TriF", + "Tri", + "CircF", + "Circ", + "ArcF", + "Arc", + "RoundF", + "Round", + "Sprite", + "Poly", +}; + +struct Context { + int width; + int height; + int minDimension; + int step; + gfx::RenderMode mode; + gfx::Canvas* sprite = nullptr; +}; + +struct PhaseResult { + const char* name; + const char* shortName; + int drawUs; + int presentUs; + int totalUs; + int dirtyPixels; + int dirtyRects; +}; + +uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) { + return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); +} + +int clampInt(int value, int low, int high) { + return value < low ? low : (value > high ? high : value); +} + +const char* modeName(gfx::RenderMode mode) { + switch (mode) { + case gfx::RenderMode::Flip: + return "flip"; + case gfx::RenderMode::FullFrame: + return "full"; + case gfx::RenderMode::Banded: + return "banded"; + case gfx::RenderMode::Transform: + return "transform"; + case gfx::RenderMode::None: + default: + return "none"; + } +} + +int titleScaleFor(const Context& context) { + if (context.width >= 600 && context.height >= 300) { + return 3; + } + if (context.width >= 220 && context.height >= 140) { + return 2; + } + return 1; +} + +int tableScaleFor(const Context& context) { + if (context.height >= 800 && context.width >= 500) { + return 3; + } + if (context.height >= 420 && context.width >= 320) { + return 2; + } + return 1; +} + +bool takeExitRequest(struct AppEventSubscription& subscription, AnyInput& input) { + bool shouldExit = false; + struct AppEvent event {}; + while (app_event_poll(&subscription, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldExit = true; + } + } + if (input.activity()) { + shouldExit = true; + } + return shouldExit; +} + +void waitForExit(struct AppEventSubscription& subscription, AnyInput& input) { + while (!takeExitRequest(subscription, input)) { + tt::kernel::delayTicks(tt::kernel::millisToTicks(50)); + } +} + +// --------------------------------------------------------------------------- +// Phase drawings +// +// The phases mirror the M5UnitTest LCD GFX test (PDQ): each one fills the screen with a different +// primitive. Sizes are adapted to the display so a 135x240 panel and a 720x1280 panel both finish +// in a reasonable time, while still doing enough work to be meaningful. +// --------------------------------------------------------------------------- + +void phaseFillScreen(gfx::Surface& surface, const Context&) { + surface.clear(WHITE); + surface.clear(RED); + surface.clear(GREEN); + surface.clear(BLUE); + surface.clear(BLACK); +} + +void phaseText(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + + const int margin = clampInt(context.minDimension / 60, 2, 8); + const int titleScale = titleScaleFor(context); + const char* titleText = context.width >= 120 ? "Gfx Benchmark" : "Gfx"; + + gfx::TextStyle title; + title.font = &gfx::FONT_8X8; + title.foreground = MAGENTA; + title.align = gfx::TextAlign::Center; + title.scale = static_cast(titleScale); + surface.drawText(context.width / 2, margin, titleText, title); + + int y = margin + gfx::lineHeight(gfx::FONT_8X8, static_cast(titleScale)) + margin; + + static const char* const lines[] = { + "Hello World!", + "RED GREEN BLUE", + "1234.56", + "0xDEADBEEF", + "The quick brown fox", + "jumps over the lazy dog", + "Pack my box with five", + "dozen liquor jugs.", + }; + static const uint16_t colors[] = { + WHITE, RED, YELLOW, WHITE, WHITE, GREEN, CYAN, ORANGE, + }; + + const int bodyScale = (context.height >= 480 && context.width >= 320) ? 2 : 1; + const int bodyLineHeight = gfx::lineHeight(gfx::FONT_5X7, static_cast(bodyScale)); + for (int i = 0; i < static_cast(sizeof(lines) / sizeof(lines[0])); i++) { + if (y + bodyLineHeight > context.height - margin) { + break; + } + surface.drawText(margin, y, lines[i], gfx::FONT_5X7, colors[i], BLACK, static_cast(bodyScale)); + y += bodyLineHeight; + } + + const int largeScale = context.width >= 480 ? 2 : 1; + const int largeLineHeight = gfx::lineHeight(gfx::FONT_8X8, static_cast(largeScale)); + if (y + largeLineHeight <= context.height - margin) { + surface.drawText(margin, y, "AaBbCc 0123", gfx::FONT_8X8, CYAN, BLACK, static_cast(largeScale)); + y += largeLineHeight; + } + + const int terminusLineHeight = gfx::lineHeight(gfx::FONT_8X16); + if (y + terminusLineHeight <= context.height - margin) { + surface.drawText(margin, y, "Terminus 8x16", gfx::FONT_8X16, GREEN, BLACK, 1); + y += terminusLineHeight; + } + + if (context.height - margin - y >= bodyLineHeight * 2 && context.width >= 120) { + gfx::TextStyle wrap; + wrap.font = &gfx::FONT_5X7_PROPORTIONAL; + wrap.foreground = WHITE; + wrap.scale = static_cast(bodyScale); + wrap.wrap = gfx::TextWrap::Word; + surface.drawText( + gfx::Rect { margin, y, context.width - margin, context.height - margin }, + "The quick brown fox jumps over the lazy dog. Wrapped text exercises the layout path.", + wrap + ); + } +} + +void phasePixels(gfx::Surface& surface, const Context&) { + surface.clear(BLACK); + + const int width = surface.width(); + const int yEnd = surface.originY() + surface.height(); + for (int y = surface.originY(); y < yEnd; y++) { + for (int x = 0; x < width; x++) { + const uint16_t color = static_cast( + ((x & 0x1F) << 11) | ((y & 0x3F) << 5) | ((x ^ y) & 0x1F) + ); + surface.pixel(x, y, color); + } + } +} + +void phaseLines(gfx::Surface& surface, const Context& context) { + auto sweep = [&](int x0, int y0, int x1, int y1, uint16_t color) { + for (int x = 0; x < context.width; x += context.step) { + surface.line(x0, y0, x, y1, color); + } + for (int y = 0; y < context.height; y += context.step) { + surface.line(x0, y0, x1, y, color); + } + }; + + surface.clear(BLACK); + sweep(0, 0, context.width - 1, context.height - 1, BLUE); + surface.clear(BLACK); + sweep(context.width - 1, 0, 0, context.height - 1, GREEN); + surface.clear(BLACK); + sweep(0, context.height - 1, context.width - 1, 0, RED); + surface.clear(BLACK); + sweep(context.width - 1, context.height - 1, 0, 0, YELLOW); +} + +void phaseFastLines(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + for (int y = 0; y < context.height; y += context.step) { + surface.hline(0, y, context.width, RED); + } + for (int x = 0; x < context.width; x += context.step) { + surface.vline(x, 0, context.height, BLUE); + } +} + +void phaseFilledRects(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int cx = context.width / 2; + const int cy = context.height / 2; + for (int i = context.minDimension; i > 0; i -= context.step) { + const int half = i / 2; + const uint8_t shade = static_cast(std::min(i, 255)); + surface.fillRect(cx - half, cy - half, i, i, rgb565(shade, shade, 0)); + } +} + +void phaseRects(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int cx = context.width / 2; + const int cy = context.height / 2; + for (int i = 2; i < context.minDimension; i += context.step) { + const int half = i / 2; + surface.drawRect(cx - half, cy - half, i, i, GREEN); + } +} + +void phaseFilledTriangles(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int cx = context.width / 2 - 1; + const int cy = context.height / 2 - 1; + const int limit = std::min(cx, cy); + for (int i = limit - 1; i > 4; i -= context.step) { + const uint8_t intensity = static_cast(std::min(i * 2, 255)); + surface.fillTriangle(cx, cy - i, cx - i, cy + i, cx + i, cy + i, rgb565(0, intensity, intensity)); + } +} + +void phaseTriangles(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int cx = context.width / 2 - 1; + const int cy = context.height / 2 - 1; + const int limit = std::min(cx, cy); + for (int i = context.step; i < limit; i += context.step) { + surface.drawTriangle(cx, cy - i, cx - i, cy + i, cx + i, cy + i, rgb565(0, 0, static_cast(std::min(i * 4, 255)))); + } +} + +void phaseFilledCircles(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int radius = clampInt(context.minDimension / 14, 4, 32); + const int spacing = radius * 2 + 2; + for (int y = radius; y < context.height; y += spacing) { + for (int x = radius; x < context.width; x += spacing) { + surface.fillCircle(x, y, radius, rgb565( + static_cast(x * 255 / context.width), + static_cast(y * 255 / context.height), + 128 + )); + } + } +} + +void phaseCircles(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int radius = clampInt(context.minDimension / 14, 4, 32); + const int spacing = radius * 2 + 2; + for (int y = radius; y < context.height; y += spacing) { + for (int x = radius; x < context.width; x += spacing) { + surface.drawCircle(x, y, radius, rgb565( + static_cast(255 - x * 255 / context.width), + static_cast(255 - y * 255 / context.height), + 255 + )); + } + } +} + +void phaseFilledArcs(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int cx = context.width / 2; + const int cy = context.height / 2; + const int maxRadius = std::min(cx, cy) - 1; + if (maxRadius <= 2) { + return; + } + + const int arcStep = clampInt(maxRadius / 16, 4, 24); + for (int r = arcStep; r < maxRadius; r += arcStep) { + const float end = 360.0f * static_cast(r) / static_cast(maxRadius); + const uint8_t red = static_cast(std::min(r * 255 / maxRadius, 255)); + surface.fillArc(cx, cy, r, r / 2, 0.0f, end, rgb565(red, 128, static_cast(255 - red))); + } +} + +void phaseArcs(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int cx = context.width / 2; + const int cy = context.height / 2; + const int maxRadius = std::min(cx, cy) - 1; + if (maxRadius <= 2) { + return; + } + + const int arcStep = clampInt(maxRadius / 16, 4, 24); + for (int r = arcStep; r < maxRadius; r += arcStep) { + const float end = 360.0f * static_cast(r) / static_cast(maxRadius); + const uint8_t blue = static_cast(std::min(r * 255 / maxRadius, 255)); + surface.drawArc(cx, cy, r, r / 2, 0.0f, end, rgb565(255, 255 - blue / 2, blue)); + } +} + +void phaseFilledRoundRects(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int cx = context.width / 2; + const int cy = context.height / 2; + for (int i = context.minDimension - 1; i > 20; i -= context.step) { + const int half = i / 2; + const int radius = std::max(2, i / 8); + const uint8_t intensity = static_cast(std::min(i * 2, 255)); + surface.fillRoundRect(cx - half, cy - half, i, i, radius, rgb565(0, intensity, 128)); + } +} + +void phaseRoundRects(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const int cx = context.width / 2; + const int cy = context.height / 2; + for (int i = 20; i < context.minDimension; i += context.step) { + const int half = i / 2; + const int radius = std::max(2, i / 8); + surface.drawRoundRect(cx - half, cy - half, i, i, radius, rgb565(255, 0, static_cast(std::min(i * 2, 255)))); + } +} + +void phaseSprites(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + + if (context.sprite == nullptr || !context.sprite->valid()) { + return; + } + + constexpr int spriteSize = 48; + const int spacing = spriteSize + 8; + for (int y = 0; y < context.height; y += spacing) { + for (int x = 0; x < context.width; x += spacing) { + surface.blitTransparent(*context.sprite->buffer(), 0, 0, spriteSize, spriteSize, x, y, MAGENTA); + } + } + + static uint8_t alpha[32 * 32]; + static bool alphaReady = false; + if (!alphaReady) { + for (int y = 0; y < 32; y++) { + for (int x = 0; x < 32; x++) { + const int dx = x - 16; + const int dy = y - 16; + const int distanceSquared = dx * dx + dy * dy; + alpha[y * 32 + x] = distanceSquared >= 16 * 16 + ? 0 + : static_cast((16 * 16 - distanceSquared) * 255 / (16 * 16)); + } + } + alphaReady = true; + } + + surface.drawAlphaMask(alpha, 32, 32, 32, context.width / 2 - 16, context.height / 2 - 16, WHITE); +} + +void phasePolygons(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + + const int cx = context.width / 2; + const int cy = context.height / 2; + const int radius = std::max(8, context.minDimension / 3); + constexpr float PI = 3.14159265f; + + gfx::Point star[10]; + for (int i = 0; i < 10; i++) { + const float angle = -PI / 2.0f + static_cast(i) * PI / 5.0f; + const float r = (i % 2 == 0) ? static_cast(radius) : static_cast(radius) / 2.0f; + star[i] = { + cx + static_cast(std::cos(angle) * r), + cy + static_cast(std::sin(angle) * r), + }; + } + surface.fillPolygon(star, 10, rgb565(255, 128, 0)); + surface.drawPolygon(star, 10, WHITE); + + const int inner = std::max(4, radius / 2); + gfx::Point pentagon[5]; + for (int i = 0; i < 5; i++) { + const float angle = -PI / 2.0f + static_cast(i) * 2.0f * PI / 5.0f; + pentagon[i] = { + cx + static_cast(std::cos(angle) * static_cast(inner)), + cy + static_cast(std::sin(angle) * static_cast(inner)), + }; + } + surface.drawPolygon(pentagon, 5, CYAN); +} + +using PhaseDrawer = void (*)(gfx::Surface&, const Context&); + +static const PhaseDrawer PHASE_DRAWERS[PHASE_COUNT] = { + phaseFillScreen, + phaseText, + phasePixels, + phaseLines, + phaseFastLines, + phaseFilledRects, + phaseRects, + phaseFilledTriangles, + phaseTriangles, + phaseFilledCircles, + phaseCircles, + phaseFilledArcs, + phaseArcs, + phaseFilledRoundRects, + phaseRoundRects, + phaseSprites, + phasePolygons, +}; + +static_assert(sizeof(PHASE_DRAWERS) / sizeof(PHASE_DRAWERS[0]) == PHASE_COUNT, "phase table size mismatch"); + +// --------------------------------------------------------------------------- +// Screens +// --------------------------------------------------------------------------- + +void drawSplash(gfx::Surface& surface, const Context& context) { + surface.gradient({ 0, 0, context.width, context.height }, rgb565(0, 0, 48), BLACK); + const int halo = clampInt(context.minDimension / 3, 8, 160); + surface.fillEllipse(context.width / 2, context.height / 2, halo, halo / 2, rgb565(0, 0, 24)); + + const bool narrow = context.width < 120; + const int margin = clampInt(context.minDimension / 40, 2, 8); + const int titleScale = titleScaleFor(context); + + gfx::TextStyle title; + title.font = &gfx::FONT_8X8; + title.foreground = MAGENTA; + title.align = gfx::TextAlign::Center; + title.verticalAlign = gfx::TextVerticalAlign::Middle; + title.scale = static_cast(titleScale); + surface.drawText(context.width / 2, context.height / 2 - gfx::lineHeight(gfx::FONT_8X8, static_cast(titleScale)), narrow ? "Gfx" : "Gfx Benchmark", title); + + gfx::TextStyle subtitle; + subtitle.font = &gfx::FONT_5X7; + subtitle.foreground = WHITE; + subtitle.align = gfx::TextAlign::Center; + subtitle.verticalAlign = gfx::TextVerticalAlign::Middle; + surface.drawText(context.width / 2, context.height / 2 + gfx::lineHeight(gfx::FONT_5X7), narrow ? "PDQ test" : "PDQ-style primitive test", subtitle); + + gfx::TextStyle hint; + hint.font = &gfx::FONT_5X7; + hint.foreground = GREY; + hint.align = gfx::TextAlign::Center; + hint.verticalAlign = gfx::TextVerticalAlign::Bottom; + surface.drawText(context.width / 2, context.height - margin, narrow ? "Any key" : "Any input cancels", hint); +} + +void drawSlowMessage(gfx::Surface& surface, const Context& context) { + surface.clear(BLACK); + const bool narrow = context.width < 120; + const int margin = clampInt(context.minDimension / 40, 2, 8); + + gfx::TextStyle title; + title.font = &gfx::FONT_8X8; + title.foreground = ORANGE; + title.align = gfx::TextAlign::Center; + title.verticalAlign = gfx::TextVerticalAlign::Middle; + title.scale = static_cast(titleScaleFor(context)); + surface.drawText(context.width / 2, context.height / 2 - gfx::lineHeight(gfx::FONT_8X8, title.scale), "Skipped", title); + + gfx::TextStyle subtitle; + subtitle.font = &gfx::FONT_5X7; + subtitle.foreground = WHITE; + subtitle.align = gfx::TextAlign::Center; + subtitle.verticalAlign = gfx::TextVerticalAlign::Middle; + surface.drawText(context.width / 2, context.height / 2 + gfx::lineHeight(gfx::FONT_5X7), narrow ? "Slow panel" : "Slow-refresh display", subtitle); + + gfx::TextStyle hint; + hint.font = &gfx::FONT_5X7; + hint.foreground = GREY; + hint.align = gfx::TextAlign::Center; + hint.verticalAlign = gfx::TextVerticalAlign::Bottom; + surface.drawText(context.width / 2, context.height - margin, narrow ? "Any key" : "Any input to exit", hint); +} + +void drawSmallResults(gfx::Surface& surface, const Context& context, const PhaseResult* results, unsigned long totalDrawUs, unsigned long totalPresentUs) { + const int lineHeight = gfx::lineHeight(gfx::FONT_5X7); + const int margin = 1; + int y = margin; + + int slowest = 0; + for (int i = 1; i < PHASE_COUNT; i++) { + if (results[i].drawUs > results[slowest].drawUs) { + slowest = i; + } + } + + surface.drawText(margin, y, "Gfx", gfx::FONT_5X7, MAGENTA); + y += lineHeight; + + char line[32]; + std::snprintf(line, sizeof(line), "T:%lu", totalDrawUs); + surface.drawText(margin, y, line, gfx::FONT_5X7, WHITE); + y += lineHeight; + + std::snprintf(line, sizeof(line), "P:%lu", totalPresentUs); + surface.drawText(margin, y, line, gfx::FONT_5X7, CYAN); + y += lineHeight; + + surface.drawText(margin, y, "Slowest", gfx::FONT_5X7, CYAN); + y += lineHeight; + surface.drawText(margin, y, results[slowest].shortName, gfx::FONT_5X7, YELLOW); + y += lineHeight; + + std::snprintf(line, sizeof(line), "%d us", results[slowest].drawUs); + surface.drawText(margin, y, line, gfx::FONT_5X7, YELLOW); + y += lineHeight; + + if (y + lineHeight <= context.height) { + surface.drawText(margin, y, "Any key", gfx::FONT_5X7, GREY); + } +} + +void drawResults(gfx::Surface& surface, const Context& context, const PhaseResult* results, unsigned long totalDrawUs, unsigned long totalPresentUs) { + surface.gradient({ 0, 0, context.width, context.height }, rgb565(0, 16, 48), BLACK); + + if (context.width < 120) { + drawSmallResults(surface, context, results, totalDrawUs, totalPresentUs); + return; + } + + const int margin = clampInt(context.minDimension / 60, 2, 8); + const int titleScale = titleScaleFor(context); + const int tableScale = tableScaleFor(context); + const int titleHeight = gfx::lineHeight(gfx::FONT_8X8, static_cast(titleScale)); + const int lineHeight = gfx::lineHeight(gfx::FONT_5X7, static_cast(tableScale)); + + gfx::TextStyle title; + title.font = &gfx::FONT_8X8; + title.foreground = MAGENTA; + title.align = gfx::TextAlign::Center; + title.scale = static_cast(titleScale); + surface.drawText(context.width / 2, margin, "Gfx Benchmark", title); + + int y = margin + titleHeight + margin; + + char subtitle[48]; + std::snprintf(subtitle, sizeof(subtitle), "%dx%d %s", context.width, context.height, modeName(context.mode)); + gfx::TextStyle subtitleStyle; + subtitleStyle.font = &gfx::FONT_5X7; + subtitleStyle.foreground = CYAN; + subtitleStyle.align = gfx::TextAlign::Center; + surface.drawText(context.width / 2, y, subtitle, subtitleStyle); + y += lineHeight; + + // Short landscape panels (Cardputer, T-Embed) don't have room for one row per phase plus the + // full footer. Split the table into two columns there, which keeps all 15 phases and the + // draw/present/hint footer visible. + const bool twoColumn = context.height < 200 && context.height >= 110 && context.width >= 180; + const int footerLines = context.height >= 100 ? 3 : 1; + // The header line is drawn after this calculation, so reserve it as well as the footer. + const int available = context.height - margin - footerLines * lineHeight - y - lineHeight; + const int rowsAvailable = std::max(0, available / lineHeight); + const int capacity = twoColumn ? rowsAvailable * 2 : rowsAvailable; + const bool compact = capacity < PHASE_COUNT; + const int visibleCount = std::min(capacity, PHASE_COUNT); + const int rowsPerColumn = twoColumn ? (visibleCount + 1) / 2 : visibleCount; + + int order[PHASE_COUNT]; + for (int i = 0; i < PHASE_COUNT; i++) { + order[i] = i; + } + if (compact) { + std::sort(order, order + PHASE_COUNT, [results](int a, int b) { + return results[a].drawUs > results[b].drawUs; + }); + } + + const int split = context.width / 2; + const int leftTimeRight = twoColumn ? split - margin : context.width - margin; + + gfx::TextStyle header; + header.font = &gfx::FONT_5X7; + header.foreground = WHITE; + header.scale = static_cast(tableScale); + surface.drawText(margin, y, compact ? "Slowest" : "Phase", header); + header.align = gfx::TextAlign::Right; + surface.drawText(leftTimeRight, y, "us", header); + + if (twoColumn) { + header.align = gfx::TextAlign::Left; + surface.drawText(split + margin, y, compact ? "Slowest" : "Phase", header); + header.align = gfx::TextAlign::Right; + surface.drawText(context.width - margin, y, "us", header); + } + + y += lineHeight; + surface.hline(margin, y - 1, context.width - 2 * margin, GREY); + + for (int row = 0; row < rowsPerColumn; row++) { + if (row % 2 == 0) { + surface.fillRect(margin, y, context.width - 2 * margin, lineHeight, DARK_STRIPE); + } + + auto drawCell = [&](int index, int nameX, int timeRight) { + if (index >= visibleCount) { + return; + } + const PhaseResult& result = results[order[index]]; + + gfx::TextStyle cell; + cell.font = &gfx::FONT_5X7; + cell.foreground = WHITE; + cell.scale = static_cast(tableScale); + surface.drawText(nameX, y, result.shortName, cell); + + char time[16]; + std::snprintf(time, sizeof(time), "%d", result.drawUs); + cell.foreground = YELLOW; + cell.align = gfx::TextAlign::Right; + surface.drawText(timeRight, y, time, cell); + }; + + drawCell(row, margin, leftTimeRight); + if (twoColumn) { + drawCell(row + rowsPerColumn, split + margin, context.width - margin); + } + + y += lineHeight; + } + + const int footerTop = context.height - margin - footerLines * lineHeight; + char drawLine[48]; + char presentLine[48]; + std::snprintf(drawLine, sizeof(drawLine), "Draw %lu us", totalDrawUs); + std::snprintf(presentLine, sizeof(presentLine), "Present %lu us", totalPresentUs); + + gfx::TextStyle footer; + footer.font = &gfx::FONT_5X7; + footer.foreground = GREEN; + footer.scale = static_cast(tableScale); + footer.verticalAlign = gfx::TextVerticalAlign::Bottom; + surface.drawText(margin, footerTop + lineHeight, drawLine, footer); + + if (footerLines > 1) { + footer.foreground = CYAN; + surface.drawText(margin, footerTop + 2 * lineHeight, presentLine, footer); + + gfx::TextStyle hint; + hint.font = &gfx::FONT_5X7; + hint.foreground = GREY; + hint.scale = static_cast(tableScale); + hint.align = gfx::TextAlign::Right; + hint.verticalAlign = gfx::TextVerticalAlign::Bottom; + surface.drawText(context.width - margin, context.height - margin, "Any input to exit", hint); + } +} + +} // namespace + +void runApplication(gfx::Target* target, AnyInput* input) { + const gfx::Info& info = target->info(); + + Context context { + info.frameWidth > 0 ? info.frameWidth : info.width, + info.frameHeight > 0 ? info.frameHeight : info.height, + 0, + 0, + info.mode, + }; + context.minDimension = std::min(context.width, context.height); + context.step = clampInt(context.minDimension / 24, 6, 32); + + struct TaskEventGroup eventGroup {}; + task_event_group_construct(&eventGroup); + struct AppEventSubscription subscription {}; + check(app_event_subscribe(&subscription, &eventGroup) == ERROR_NONE); + + if (info.slowRefresh) { + ESP_LOGW(TAG, "Slow-refresh display: skipping the benchmark"); + target->render([&](gfx::Surface& surface) { + drawSlowMessage(surface, context); + }, gfx::Prepare::Overwrite); + target->refresh(); + waitForExit(subscription, *input); + } else { + ESP_LOGI(TAG, "Benchmarking %dx%d, mode %s, step %d", context.width, context.height, modeName(context.mode), context.step); + + gfx::Canvas sprite(48, 48, DISPLAY_COLOR_FORMAT_RGB565); + if (sprite.valid()) { + gfx::Surface& canvas = sprite.surface(); + canvas.clear(MAGENTA); + canvas.fillRoundRect(4, 4, 40, 40, 8, BLUE); + canvas.fillCircle(24, 24, 14, CYAN); + canvas.drawText(10, 20, "GX", gfx::FONT_8X8, WHITE); + context.sprite = &sprite; + } else { + ESP_LOGW(TAG, "Could not allocate the sprite canvas"); + } + + target->render([&](gfx::Surface& surface) { + drawSplash(surface, context); + }, gfx::Prepare::Overwrite); + tt::kernel::delayTicks(tt::kernel::millisToTicks(300)); + + PhaseResult results[PHASE_COUNT] {}; + bool aborted = false; + unsigned long totalDrawUs = 0; + unsigned long totalPresentUs = 0; + + for (int i = 0; i < PHASE_COUNT; i++) { + if (takeExitRequest(subscription, *input)) { + aborted = true; + break; + } + + target->render([&](gfx::Surface& surface) { + PHASE_DRAWERS[i](surface, context); + }, gfx::Prepare::Overwrite); + + const gfx::Stats& stats = target->stats(); + results[i] = PhaseResult { + PHASE_NAMES[i], + PHASE_SHORT_NAMES[i], + stats.drawUs, + stats.presentUs, + stats.totalUs, + stats.dirtyPixels, + stats.dirtyRects, + }; + totalDrawUs += static_cast(stats.drawUs); + totalPresentUs += static_cast(stats.presentUs); + + ESP_LOGI( + TAG, + "%2d/%d %-20s draw %7d us present %7d us total %7d us dirty %7d px", + i + 1, + PHASE_COUNT, + PHASE_NAMES[i], + stats.drawUs, + stats.presentUs, + stats.totalUs, + stats.dirtyPixels + ); + + tt::kernel::delayTicks(tt::kernel::millisToTicks(PHASE_PREVIEW_MS)); + } + + if (!aborted) { + ESP_LOGI(TAG, "Total draw %lu us, present %lu us", totalDrawUs, totalPresentUs); + target->render([&](gfx::Surface& surface) { + drawResults(surface, context, results, totalDrawUs, totalPresentUs); + }, gfx::Prepare::Overwrite); + waitForExit(subscription, *input); + } + } + + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&eventGroup); +} + + diff --git a/Apps/GfxBenchmark/main/Source/Main.cpp b/Apps/GfxBenchmark/main/Source/Main.cpp new file mode 100644 index 0000000..df261f3 --- /dev/null +++ b/Apps/GfxBenchmark/main/Source/Main.cpp @@ -0,0 +1,127 @@ +#include "Application.h" + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +constexpr auto TAG = "GfxBenchmark"; + +// Shows a blocking error dialog and waits for it to close (so the user actually gets to read it) +// before the caller finishes this app - this app never creates a window of its own, so there's +// nothing else keeping it around for the dialog to be seen against. +static void showErrorAndWait(AppInstanceId appInstanceId, const char* message) { + const char* argv[] = { "Error", message, "OK" }; + uint32_t dialogInstanceId = 0; + app_start_for_result("tactility.alertdialog", 3, argv, appInstanceId, &dialogInstanceId); + if (dialogInstanceId == 0) { + return; + } + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + struct AppEventSubscription sub {}; + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + + while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool done = false; + struct AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + done = true; + break; + } + if (event.type == APP_EVENT_RESULT && event.result.launch_id == dialogInstanceId) { + app_manager_stop(dialogInstanceId); + done = true; + break; + } + } + if (done) break; + } + + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); +} + +extern "C" { + +int main(int argc, char* argv[]) { + AppInstanceId app_instance_id = app_scheduler_current_app_id(); + + struct Device* display_device; + if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) { + ESP_LOGE(TAG, "No display device found"); + showErrorAndWait(app_instance_id, "No display device was found."); + return 0; + } + + // Stop LVGL first (because it's currently using the drivers we want to use) + module_stop(&lvgl_module); + + ESP_LOGI(TAG, "Creating display target"); + auto target = new gfx::Target(display_device); + device_put(display_device); + + if (!target->valid()) { + ESP_LOGE(TAG, "Display doesn't support direct rendering"); + delete target; + + if (!module_is_started(&lvgl_module)) { + module_start(&lvgl_module); + } + + showErrorAndWait(app_instance_id, "This display can't be used for direct rendering."); + return 0; + } + + auto input = new AnyInput(); + if (!input->available()) { + ESP_LOGE(TAG, "No input device found"); + delete input; + delete target; + + if (!module_is_started(&lvgl_module)) { + module_start(&lvgl_module); + } + + showErrorAndWait(app_instance_id, "No input device was found."); + return 0; + } + + ESP_LOGI(TAG, "Running application"); + runApplication(target, input); + + ESP_LOGI(TAG, "Cleanup display target"); + delete target; + + ESP_LOGI(TAG, "Cleanup input"); + delete input; + + // Restart LVGL to resume rendering of regular apps + if (!module_is_started(&lvgl_module)) { + ESP_LOGI(TAG, "Restarting LVGL"); + module_start(&lvgl_module); + } + + ESP_LOGI(TAG, "Stopping application"); + + return 0; +} + +} diff --git a/Apps/GfxBenchmark/manifest.properties b/Apps/GfxBenchmark/manifest.properties new file mode 100644 index 0000000..fd0277a --- /dev/null +++ b/Apps/GfxBenchmark/manifest.properties @@ -0,0 +1,11 @@ +manifest.version=0.3 +target.sdk=0.8.0-dev +target.platforms=esp32,esp32s3,esp32c6,esp32p4 +id=tactility.gfxbenchmark +version.name=0.1.0 +version.code=1 +name=Gfx Benchmark +description=PDQ-style primitive benchmark for the direct-to-display Gfx library, with an on-screen results table. +app.0.id=tactility.gfxbenchmark +app.0.name=Gfx Benchmark +app.0.binary=gfxbenchmark diff --git a/Apps/GfxBenchmark/pics/phase-01.png b/Apps/GfxBenchmark/pics/phase-01.png new file mode 100644 index 0000000000000000000000000000000000000000..48117d78aebf34dd3ff9cc81432665f4ba750284 GIT binary patch literal 2900 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 literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/phase-03.png b/Apps/GfxBenchmark/pics/phase-03.png new file mode 100644 index 0000000000000000000000000000000000000000..e67ec0ac9bf1eb492c9331cdb0e1961013b41fe6 GIT binary patch literal 5420 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 WP~?4U-XJ~M`{&Mo zss^(f6nzc|s3mX+8?F&_4s&EQlhY70-lTKX`W1Bx<+emwiW8XWr;Nt14!5 z${b-Ul1lA@T%dFHVq9jn-CeQmfw~FYX>dhA7Z-Py7JN>TT6cSrIS*b{?_XQEAkBEstlfK2UjCa&aeX|0iBh zm|&>nin(23+RH5-xGd3pmF^EtUQ9*6@Qg{4y<;=eE!({9-t-Dn-rXB_9%*v_$%bwm zSS9Pa+wRUR&gZrx3pB5~NfYGD3kuPta&*H7p&hNeV%xb%PBPQwK;u>a9RG>wq z-bSYDW^ulc54lbO26q;Bof8&CK%Y&IU^n+)cJWs4!tbh}NYByR_#kj!Bf4>5m5tfb z>z5TiR61qy#%(LB{X#5?fT_ZJP4jH^%NcKvUHC1u9vD7m(<44;+(%Dlpdj^Lb6{(4 z_~ji1f~itvi*Gf}zkrq&A>n>T_1eL+lQXxJC66XYEXi@rI{PjWd4(e7$A5uk1B0il KpUXO@geCx1nV~xX literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/phase-05.png b/Apps/GfxBenchmark/pics/phase-05.png new file mode 100644 index 0000000000000000000000000000000000000000..c0b358086021263996c7da5a32343702488942fe GIT binary patch literal 1151 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoKX5Ps$$$P@HVh0bZk{fVAr*7p-o2P594O*& zQNzw>iO1jn{UI(sa;)-4_{^5PR4M*`Cd&ST<4qZh7(Y9iuurd#N7YMC`~LHEqNx1! zpPLT|vG61^wHYuv-&lTFV&DIm-`o#$=gsF+HfU7zIUt~xz#(kV(!1%%EX(=V|1ni$ z&f;S>LXzR#1(c~=e)O>H@rvxynVe9wW;7_S*?45uN5AgBd$#U6!vZoNCb^v8|FW;IR{D1A$R5gPZplYy_ zH@wx~n09XY*8L6Dv-*Dj`fiHkthafcrjO@^-Ti#+epGB46Vz^?XKw!%%06bh_G9jQ zGzaKx)GPGQW|_S|`>Pz1B?jJW%q@Q3J={J6=#N=Q-a8}oy6pa+cMqNRRb`iI3q#!m z@}}ywq&=5+&D$R4ZHVNe$fUd7)lvV8uD|m00f#BX8`C2Ai|k~}QmxkgW&NFi_xmpgO%_>%Gj%Y*RZF6T;5_1(`cF z8O63+U#)fX*Sqc=11>>Le7s zyTr@aRxqIW{aVtUt&cvUdUAX5^{%VPDNbkOy#vwOZm8k2>qx9NTRMvQRSzxJewAn3 zj+|58)^>jVv*nxk0c+$yE8Bi#R$q2Kqt0<8pKSZ9I&F7Y&I}}<1f5;B-Z{hPj>ZfI_0iWECqXB?`|p<%ad_vFVwEe*DJ`Z(W(|7TQ)KF`3U_PoyL!}+WA z3=c|~+6)+-Z|pzrV7!fcM*UVP$qy&gkG^|%+`%n_L)f5&Q{vFFcLyGq^KUD#ZqsIo zsp!*rQFkW21*B_6gQCtR=Ids{tMc5v4IVrQUiPr=iNr!hp92DF2^`t)4t(6(>X_#q zW&hw`{(;nYKhyJJ+HyK&E7q=UE519e>cDB%J$LkvSl`vqKdzC*n-ad6c z=W9dw|Gzi>aOc4_H_KKm@9ns|PHr~qo}24;FaEtzRM`aNQWl=X-)|2{zxO`9OZN6H z>j!)5f4ko4pDPJ-((O9-e|hh7R~>o!sJr3+le06hEzmu>Zl_ zYVr8P;czQAGw<)zeYZSD;GW}w_m6vP=Doi=8y-ZPndcww`(B^NqAu_ubI#G7j@-gNeIW46-@WCluSWG;03{7jx~j-5_kRBf7`p2&cz@P+Tm9J_oY;T@We=YI z*dJys_#rB0tr?Ovcb;c$HDo-#GwSVGgbfxau6F4&tt;I62st8|!nNfDK2)p=vq6d_ zn@Ouh&kKIA0g56SP{9_i9jEkwF=lPWRHOiO1@hS$;}2f#GBmwPYP+JbPzJ|%JeYL23A|hD+*sTkznbUCp$ki@wq$vO2v|2P=_`~X$wGsyq z{`jE|4W3+#kb+iJ>zWSoAO0Ea)?}!S72T#s#*&A28Htl6D z+o}YQfE#tz8&->Q-_%J6*S-c%0y)hfrKM}-fO6i(Fwbp!#P=rasM$1bpxj)zbEA{k z%eE$k|80m}djp;uHar6=WxWa%d(aIFvJEHTVvKP22bTjaUcm-bd>$T%W@$hZL50|Z aiU;;a8eVminD+PrxeT7JelF{r5}E)$1S+Kf literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/phase-07.png b/Apps/GfxBenchmark/pics/phase-07.png new file mode 100644 index 0000000000000000000000000000000000000000..abe78200be83cb1f58083bdd3faf51b36345df42 GIT binary patch literal 1674 zcmY+Fe^gUt7{_-nLpIoeJ4Gf=5e8_QZpg0*C<H3X9BA5DCK&b{!s-HbB&r{E?gFKvKCm6t3K$oAaOkdEf6p&v`%3=XuU~E7nQH zOjjRQ3WdU4n;=Y~P=M8B#%LgUP9-mnrch?QyH>bnyV+&*L}U$|xG0?R#ot;RV((=;(HALZm!;8=Nm5K}E(4r6nM~phLtr=ja27k29ArYHr=8=kCkDwr1@z!dz z|Ks^@{T*g~K@$`y`Wh>Rp!D`b+r#GSpIO)oGcTJar*L%=^!??A_5O|xl64U&YULHa zv4UC>Y?rJA^_OT@AS~O=3#7~0TwOfs%{QzMv=plmPhh$dZ{P!(&8qjJ=kEntTeS~v zJRTbwEoiEvzs;}C7j-62B!bUk42&SS@_v;X3!@V5c8LpUa}l-81*X+_10Ap*h4SOk z0NDsJ-Vh8F268jHC%#CFpBkUserrTyn)=nUZ??9*IPJ=|EiM^33Tse{ z)0U;8i3JCo!VO~)0{B+8O2ZF>?Gn{Vpv9Scq5Uv$OqQZ%XrHpJdwZfPcHSiP(i46A zKHSrc1-qFq6SEqYGTkOnyp+js)5fu_JxQ(u1S=;6E;rtmL|3%lCpvYl&oh>|Hlwp_@U1pwM4fC= z&@$M_wyq%?DC>6BCWz!hlBRe+7^H>VJqC#7DT(N%5M%Sg=2V@mul2Z>TT*(mUhCzF z{bF}JJ?5e9)_H(M0AwBKA-j&{bMPK+k5M!Ama#T^njJDBsJGnKgex&t^JuPt5?NVEjk0 ziQ#5WkX)t3P|ZCNC$}>>dvlA^HC$sj-0sh2MyBT$gUqG zYOsP6{*@g(tTFj3X?7Z%8yy}7HjW)!%?$&~KGOe~B>hxry5N0{yz0LhVs#bnK*RoU w&K((6);A3e{SJ6f5cOehj3Rz41@Re~p?m73)+%w*uJM@rP zBm@G{qqsWLAP`^+$fIy5sPkSHju41W1I2mkuK1kc9;@>&w_|wBwK4tK1=AjZDi4}w zP1%vOj+!$8i-A8FRmL@e2kU3=U4C2q7aSh;q&7EbKsT-iEcAJKiK-v5G}qO-%7l(3 zV}J}ZuI;_f#mk2d*q|s1LjuEfoLzzCp*R6Xq%b;rx7535dm)tf;iVsTMJh#jiI^pu zn*I^pH(l%G?tm`#AJ z(#D14$E+{O-d&5#OUM|vc%lnHM(7s_nEZZ4Mu2~d;y&C4)nh96lC*fFX{}MXTSzYn zsdbpZsf6l1+Nj0(DvUVKH@6RytO`P4k{5N7U|j@8 z<5Id59azrATPmIGdTsbd^3|P;e80S|A)WE_A*V>NPoqRe2QN8wTz!u6%uYVamZh;^0-P8F0!`eL0k;j5YJ_=mkr(uiVzc?XMk;1@X1~l#h3Q0r6|FpI<=pSv9YJh$-wk4O6o?1i=^!@b zfVj*(pq$I%*Qd0UGEevySj1{OU)^=3{GbepUmuk3hWvXiw#4 zw~%lGkrB{-oV&b@R78sofDY_}9`KarIE&@0loRgrHZjq4a8;wMD@4wznH#V3Ic0ZS zEDTAt>neF=LAum_J_Hrsn#+|JFn^(`{(-qup_}t~kz{cYoNo*3KFhOr6XOv4d{}oD zFCi!Ku0ag7(xbvLZZc&K6+Xt4I~=utU{LwYMY1Q7&k=}z1}2sQCPhr&V$o)x(oM3= zx(PSvfoF`MW6*lPlEwZot(DX(v*f$+nOa7;oR3CKG-YnOJo(nT;Z72iE2p>EeK|I=A^JS&}L zZ+^=(o`N@y^#o1BP<&ApXd*IW!WVWK2y+o=Mi^h{mtYQpbRFd#A-z=nfEUcY_eJ;PC~4v*4kFaG}R!Y|sAa zx?NAM8hOPA^~O26mi<;yJyK@YnyFa1DwlA2*u-7(?09hpl|xnOqnoPolMB_lr{9$L z)VFCCImbQ~u-?xB52}OHZw8i>*t2QsM5_w6O8@K63y~Cx?*W;>r)}A#J^2e>ii@Xn I*)~S@d3! z%9_eB7!0x7YWrRoOl}Ks?pB%w>|?g8&0w&_&bzm7-4~k7ciQ0ZWUTg8c(vybMvRV2 zv(34X*eav>CoY!7M>EddIh@ZZKl_%nNcBVknk!bd^`zHyvwq_%SzhSE+5d*R+6iozjXM*J57`RB^ib% zI@B>fU+RAbH)1mDH5Cb-kl^W-d$xMfJB(@AUD=y|0bV^Nm|c~FA-1#_7@=yhr+%e8 zw4pq-ri5Ej!uL?v>RHZeS==pH&%ssi9g)U=h5P7c93=R){NA1&mwcQ-yZT#BvC;7t zoD}F3T*Kh7)d^b+FTE?3W9nnziu=kpA(q-9KL2uT>Scq~oK1H$EB`(pL=PC!-!>uc08dZjvsq0P2|P71yyP4$?L29Ey~Jx*uv z*dZu5}CSMYkv_eWb@_YWyf)qq}6J5M!5h7k<#9^8gYH*XIZ{c)s~ zQGN9G)GK#F0`z!|7^I*PO-t9gxqOYkOMgRaV%liYgI3QHznpI$2uVH>Q?Ix`k3Zf@ zV@A$)_4{etoF`AN04LYyrBQkMHO39!cC7R?Ey-;0ZCi9BnLl#(T0zR8eaDOXF8*2* zJT``fUfQ8>hvv?0e2TW)cZNQwa6$RlqNXkOa-Ge{pH`PTorLe0eg7iiP9`j*2eDw` z$90b*@49s~Ti#Jxt2DA*iX;DDQwwVgN7*|e=KKrr4Ml?DwzOV&_Tk3FI`TR~46Pd}IYaxqF0c$7b0lffVRKTivQ0z!-Mr6hWXoC28eLUm6I3p5Hc1>28 zH}w+fB?p4LjOvfZ_@p0F)u_B3G7ZQ44|xNYM#%ngX8C&130~Ah@J02D2&8XKJV9?j zy!~?~td6!9p;x$p$6ck5ikGZ4=>$_+w|kR<%~u zK~(otM(XfgGwYvK6g%UT{lqtSx&2`&%2Y{e~Zn~r0IebtAW z^{Yk9o)(fJRx{Np>lGBf!{V&z;BE-+(Xor3$|dw|a6j@xASE%){2BS1)I)Z&XD!t` zg16Lq8BSFrX6_oLc4We>yN({t7yNIBt$KK*_1>48x5XFXJ_(}Ru4M_IKIvj&V&gY0 z(c0X6ICSv*b@2sv-o$P1dqJ#HoFK0{M(XPJ$agjOEpRob)V@%D()&e-TqPn@p?_iC$|*?vJHPLPZf z{EiC(gkwu#t7ZACWv#ojdQ`hUYD~o}dQ4@+N2p?U7R0dSB>c|uHY9Ae1gZVov%!~n zC34G^p1c?w%&nN0oEemq8FVXCa3xcakV!*%Ggf#r!U`ig=bE{VjIhcx9w?@CtWfHoFJ*9%N=zkQ*w< z^a^rp1^HP8*__H-N#!B-1;wO^MG4`^Wj`0&w2^ZF@@iXSL$-O3f^=buElP_kJOdJY z8CQ(VpuJwNH0Sdxy0#>ak_rvJ4#`xmp$lE4J}g6*Q((=`*a4Z8LTn0gku6QgqxHyf zt+qI)fD$}eqpJuOwwDVNZhMgj2uu4OvE4LUIHJIemp1e8|%(0v5~j$ZXfJufn(mGITiw zR@c6xvqdJg3+WwOU?N4BlcaK!MK1k2gckeWDo`smE8K<>j-iADX;9CBgL3dAVbLV) zBD))7D!yBVidx%*ieogPV(Ls`PLfKz>j2ucz6qu(?|S6qNQj#vrtYCl-9ziTaI3oT zJ#}n#nzI^>YlgQ7QPA2DG4?sJwSGUCTx%ghj{y#K1~KJ=BISUO+>aouOMO!Ez|aaM zR~cIpGx1B|J4|p8TNjl^H4e++{pQB>b7MNWF)iJg?rzLfoZuQxpg=Y$w|yXiqhz@4 z?4&?3H?Wh>N)4T9%4zy!@5Dp7cNe80?j8QiqUdu0tsGg27GjoO1!UX&4`=uT$Oh6W z8M+i_+fLTIAEDUGVsx46%a$hO(GCQp)jh{^Yd{J9QlS?86!A*7G$D@?pF&#g>n#9+ zA_cMIDBw$wOE5BOR2s`nv=FmRC1fz}F67C#UyO|W-vaG8xR;zxDowLA_^Ch&Uz*hl z*-_fTCu!pxiX!U{DNK{4ggv>MBBu@!6+I+nDPhlIxX7uGM*>E5wz)%c1O+*L zJ-Y3SQ)vKJdqI9wp4pIa=Y$i#lWp_D3Zq1^H`6riwDK}3!kmn@gc^}cztY*|K7Sgk z)M6QW4(2Ea^OX)x>RFxYSw$7(qzdw!%wL6yMR$a7m2@Fo$QHtDX9{yN+M3({4BE7V z08@&}+qiiQMTMihIeOllmEPtnyv_Bz`6T>$5}u%yon{J;Uu+?aunFvdTBavRs4Yk+ zB+@RBP=D}|00DcMhN;Jx@>@uJF zQo?sr!r|DIPtv;Pq(VbNz--H98xI8g*gpDR--Pt(r@Iykyt*uYrPtc8KyljC*C{TT zGZmW7ISlpD(*EH%e#PUd?Q)7P#YHrv-Rr7<2z{$&J9l7Ng4CudE}#7|P#4vEu-_Au zpr+doPns1xq&7`)`D8RhT{P_50U#){VtyKRmQb{(sC*7N@jn^9O^$6NJF91<0zYb5IV9`=3G3*_v~gq3-gi$3 zcYH03j0M(dzA(~rvSCgNb0XT7rDom$>gTL}G)#UgZi%)qsXkJj=C4lcRHJDDKk76f z_ukr`jgDv4rbO<#ly;i_FXR4&;4sJloc^FdLLre3fLx-%M>Kd^{|t}v%}5FE-7b6M zc+m8~(Cd`a|Gdi67n7Pb1O)UXT+l6?;0jKVfTN)t7%LnYk>r~0@zKvDj*{~sx+yd% z;wKa7G!$1x(G=^m=&A-ZA)4PF{JBtL2&ikxVN(jGWTFL!La+Vvq4AK*@MvYQThsSO zK@hBTL7^2mC5j8`8lES;X^P7yu^jr1>y23kpacjTQwpYJqJ@S+1G!N9JZDliTY0u&qQ2`y}MoOugllJ7F5f#w&B~GW$zaEz(D9GuH zpoe6Y8v$#GLRr2heyrr`gcGHXg2svT)ykfj67ewHv~9a&%z_|NlL|y*qS2iGZcVS6 z@<41Z30q6T+LEyEE6BqYed(*h=m=T$^@60=D+s1S{o&Dy+^PcB_IsZ?;_jjJ>iQkE_TP&+C zi$EY2+i$gYMj+<>1mz<#643W=$2DjKLTQD)^_JZyGFc=e4AS9;n0^nxu?DbK22+ep zSnmnMRLs9`OIR2a1Nx|Tg`Bo&mrzP_p*S_lfU9>orKc1aU{4zCS+ZC6rmaJ-$EL)m z$%lD^bj#^BCtMk5(1NvNFn?undmQiRUg<_IL?1bLH+}bxDl}M;7@n zx@J^tlu4t~K5c}kb*f^#J-sV-XD3at*A@L{sn9JeDj6|u%(pjmTX8~DYU#f6{nqEg zFWDRum9#E+cA(?%64FWh=H+|wOE!oPic9xZI=x41jNh72sJgEBX4=7q=lW{5^DBM`yc|}VZa9ezqiynJm&!a*LvLGUxJ>Wio zuGiui9J3>;Ax+Y^5Y@;Q>DjrhTPfMCTYXG4jxftop7^4&uPV9vqPha>n-)@XCa5?Q zt&Pcjl96(#DMqF0w3nlP#H)LEJqhOYHhpp$JGXf)qaOERD4NylmUyKF`y|=f(BJTy zduJ;abN6x!)+(+LVK!3GL7nz8D`tgPb5U(r7k>caIh7S2MA%#m2Koa!WV#mtEC%Jt z^i2%EAOh(E_6h*A%DN*UdYQ*1BKF|CC*6JmqAMc<1Vo#`w|sEWGJ-0R`2ap{rI_;h z#~2fhG`G!$v$o0%*d?mxnp9f^ylza2PU?!jc8lS6hVUS}#mJhTW)awaK{lYiFRt!JQ72&Vg2Nd6$|s*wAf@aTIJPQvHir3l z6Cr@T2X;L|Lp;Yr&rdw3D4oxV)M?1&hm`UgyDXjX;ITZ6=H?qjh-aD6GtUYx%HV%D zPR8c9Pu7>>RZz*VVxN>vISpGyn50)*5!Fo64={;psEnjBjf|vYwq+?~3ys0Vt4c>v z*|ArZhRA5rh<=7brX4$)?Q1bop7nr2N*Fcd-VW_&-IXJ!g%yHc1ar%6kXkd#hczLD zYv0GSdZ%CDZi=ITP)fV=Lu?UtNX)9<3DcjJ7Zn82Up$-?>p?Z1`3x zw2Y8OQq9ruF)xgd`|BS+mvDOG=*~}QyM@j`zAPa`1iv#tS0n}rMD2F-8EsaE`FNjD z8*I!|DN;l$oROWCVLsmPBnFBD%i=H=;AMGzFiu^3bQXj(G5tct2H|Bh5oOijD8j!p z{C+g_oRGgm$RtR}2fj_n5kess!9pI+fQ9T13z^~v3mLxUQwc2vLWYl9J2%V;d6o)e zPRQROWDiKl9^WQpTn8-VBB7A0U?F3LLT1834ufx{LQ8>=-Go98pA+(2h5SEM$oY_v zy}wP!X+j~F!a_C_R>+5hLiUG+4Bzspgq8v!!^f>2a~1MzE#I7wzeC8wEGdf;jTUZ| zCt-O2piiblWpX{YsvLw0Ycn!kiQ!iQVq37%3?_+nryRuC15bYu0E=V5hV8xw`*;Y( znFlZ1-@(h$AR)%^_PcTZKM+L}%qBiBi$4x9!s=>;ql_>l?LWQUOl{2X{<@9t!9$E2 zRvPU718yrKHEB!#5|MNr%G*xFR#=Uo7p>vfzk+5?WFnU(=>I0h@Tgm3E02ngQvl5Y z0P4iX@cvy;Ph?~{ODXZOHs%-03@1rBBlCZaCBEqIK29$9om=Ql=6J#OaNhB%uY8UO z<{KKkIPIDmU1%nfD1j7_^gvCXD%9j9TjcM^fu1)rjFfy+ZK}VNG762%5D0r4jCGNf H=jnd{;ol%1 literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/phase-11.png b/Apps/GfxBenchmark/pics/phase-11.png new file mode 100644 index 0000000000000000000000000000000000000000..69a8cbb6f2ec3a5aa197dcc1e0191cbdc024ca62 GIT binary patch literal 3269 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 D;0!1@ literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/phase-12.png b/Apps/GfxBenchmark/pics/phase-12.png new file mode 100644 index 0000000000000000000000000000000000000000..b94d50722200a40c43252450ceacecb39dac7fc8 GIT binary patch literal 7340 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 JS_SKf{{zj*Yb5{x literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/phase-13.png b/Apps/GfxBenchmark/pics/phase-13.png new file mode 100644 index 0000000000000000000000000000000000000000..498e17df2a49f1f3b53722d9737e2b931bcdaa76 GIT binary patch literal 1796 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 literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/phase-15.png b/Apps/GfxBenchmark/pics/phase-15.png new file mode 100644 index 0000000000000000000000000000000000000000..25525b1e7d11eb3279bc1064f4889ada8e3a8481 GIT binary patch literal 3678 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 literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/phase-16.png b/Apps/GfxBenchmark/pics/phase-16.png new file mode 100644 index 0000000000000000000000000000000000000000..3180c2c9c169c1d1b0457c48e569b4966794cd6a GIT binary patch literal 1655 zcmah}Ydn)}9DX*7Cd#B78&Y(nC{`^Z$B15v4h|(rIZlMB$tX>7XeDYa_IjtI(!q?` zqba@ZjhvF$<4H6n%wg7K>)raj-`)@1AMW4(`rp_8x_rdI^?WU=2pbh|lriZ(; zF90BRC~DDsq~ezQtic0-s=bG^Q=U%F+%(xQzQ;S7U_eVPa5s$S#((W?TF47z!3Ut5+ieMJ2>xle@XDtmK>Tyc^(uS9zSK^FV+har%m7H?;EZb*SRE}4YCaRCT}cc@O~ss zBi=T}2U}4D4J^6t>kUr2_xf)h;Ow1P2%)o>>7G*3LDET@`3j$~l37lgJ7njbYGG7_ zVP6a?J`<+sYtkA>#--UwXBgVKuT$atT28M`B)`s|Y325GUGTKZL|9G(UNo|*7v4Qy zP%|}j&V^`#z~1QEPT-vW9y;k~mpnr9NHj+F5j5;q#g0A;GfLt63eE$Iuo;q*oU%Kk zqlVl(b1?mmI8`unI6?5Otded9Vjj=#`&sgjnZ(;uEI=iLdi8wpP2QuKAOmhJWunh{ z_`M19iyOw$G&@ei!IfAnuIZpcqetio1vXmo|11vgX!y{f=#6v_hzAvc&Eh5!dz}Ps(6!79P^3Z{0qVJjjW%U z#OV9IxYph~hn4Q2bJr#=taufd{-|~&hzi9n!Czi}oE1FrOuiR5IwiU1bo|5Tjk;er z89>sTxcm|?^w$95xEUyE=gHj16<=4=A9Ey1v|vcz67OQX!NGIfTW-yZ;a6v}=N_sW z-p4Z1jgJ)C7f#!Iol^^A4eH25bL%**T5$81k3x@eD(d@c>$y&b5XoSoMMWCAH_MHS z=oJzz-C-YahBsP5c z>u$*+8Djw=cX^vmG%0uIIn#}Z3r#3ozK-7$=@uPj&7vMCyk)nvJQV})C;PO07Dk{H z5m6?Chu-FLldhr-W8Z}Yku;_-uw{$QddJvx&Pk>qW*)8lP3uOqGSP~>t1UU1ZR?OV z6l-=qn4C*J5_%)*L_%imX~(nyl`#VwCq=>)Yv4a}&E_AL5r%6F7sD^j`{wO-Lk9Lt zPRiAtf^6eEpVZgOwpdkTPn+_CYrl%GyHp4#y1HkpVU8dxB{rhao( z&2X^qECY&;+Ih^zfE7O?7uP)@EJ5M}5*+%-dO{1(##O?*Pa%XO@v(|lwVrSV_}ERt z^TB+Iy65B;LWb&|pShY5aA&=9O}Z1{+j#ArB-Z*dx~-U51m5)Ucb|`!$hxF k%MRnMf4|iK$Dox?7?YFscbi7EFH=5xxOh81a5_x>7drzEj{pDw literal 0 HcmV?d00001 diff --git a/Apps/GfxBenchmark/pics/results-320x240.png b/Apps/GfxBenchmark/pics/results-320x240.png new file mode 100644 index 0000000000000000000000000000000000000000..cc23ea189c66d330c8626185d8457ae76354e64b GIT binary patch literal 4274 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 v;Wx+Y_NU;i>JN>1j``o*1%RR+>H( +#include -void runApplication(DisplayDriver* display, TouchDriver* touch); +void runApplication(gfx::Target* target, AnyInput* input); diff --git a/Apps/GraphicsDemo/main/Include/PixelBuffer.h b/Apps/GraphicsDemo/main/Include/PixelBuffer.h deleted file mode 100644 index 1901927..0000000 --- a/Apps/GraphicsDemo/main/Include/PixelBuffer.h +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once - -#include -#include "drivers/Colors.h" - -#include -#include - -class PixelBuffer { - uint16_t pixelWidth; - uint16_t pixelHeight; - enum DisplayColorFormat colorFormat; - uint8_t* data; - -public: - - PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, enum DisplayColorFormat colorFormat) : - pixelWidth(pixelWidth), - pixelHeight(pixelHeight), - colorFormat(colorFormat) - { - data = static_cast(malloc(pixelWidth * pixelHeight * getPixelSize())); - assert(data != nullptr); - } - - ~PixelBuffer() { - free(data); - } - - uint16_t getPixelWidth() const { - return pixelWidth; - } - - uint16_t getPixelHeight() const { - return pixelHeight; - } - - enum DisplayColorFormat getColorFormat() const { - return colorFormat; - } - - void* getData() const { - return data; - } - - uint32_t getDataSize() const { - return pixelWidth * pixelHeight * getPixelSize(); - } - - void* getDataAtRow(uint16_t row) const { - auto address = reinterpret_cast(data) + (row * getRowDataSize()); - return reinterpret_cast(address); - } - - uint16_t getRowDataSize() const { - return pixelWidth * getPixelSize(); - } - - uint8_t getPixelSize() const { - switch (colorFormat) { - case DISPLAY_COLOR_FORMAT_MONOCHROME: - return 1; - case DISPLAY_COLOR_FORMAT_BGR565: - case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: - case DISPLAY_COLOR_FORMAT_RGB565: - case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: - return 2; - case DISPLAY_COLOR_FORMAT_RGB888: - return 3; - default: - // TODO: Crash with error - return 0; - } - } - - uint8_t* getPixelAddress(uint16_t x, uint16_t y) const { - uint32_t offset = ((y * getPixelWidth()) + x) * getPixelSize(); - uint32_t address = reinterpret_cast(data) + offset; - return reinterpret_cast(address); - } - - void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const { - auto address = getPixelAddress(x, y); - switch (colorFormat) { - case DISPLAY_COLOR_FORMAT_MONOCHROME: - *address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3); - break; - case DISPLAY_COLOR_FORMAT_BGR565: - Colors::rgb888ToBgr565(r, g, b, reinterpret_cast(address)); - break; - case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: { - // TODO: Make proper conversion function - Colors::rgb888ToBgr565(r, g, b, reinterpret_cast(address)); - uint8_t temp = *address; - *address = *(address + 1); - *(address + 1) = temp; - break; - } - case DISPLAY_COLOR_FORMAT_RGB565: { - Colors::rgb888ToRgb565(r, g, b, reinterpret_cast(address)); - break; - } - case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: { - // TODO: Make proper conversion function - Colors::rgb888ToRgb565(r, g, b, reinterpret_cast(address)); - uint8_t temp = *address; - *address = *(address + 1); - *(address + 1) = temp; - break; - } - case DISPLAY_COLOR_FORMAT_RGB888: { - uint8_t pixel[3] = { r, g, b }; - memcpy(address, pixel, 3); - break; - } - default: - // NO-OP - break; - } - } - - void clear(int value = 0) const { - memset(data, value, getDataSize()); - } -}; \ No newline at end of file diff --git a/Apps/GraphicsDemo/main/Include/drivers/Colors.h b/Apps/GraphicsDemo/main/Include/drivers/Colors.h deleted file mode 100644 index d068574..0000000 --- a/Apps/GraphicsDemo/main/Include/drivers/Colors.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -class Colors { - -public: - - static void rgb888ToRgb565(uint8_t red, uint8_t green, uint8_t blue, uint16_t* rgb565) { - uint16_t _rgb565 = (red >> 3); - _rgb565 = (_rgb565 << 6) | (green >> 2); - _rgb565 = (_rgb565 << 5) | (blue >> 3); - *rgb565 = _rgb565; - } - - static void rgb888ToBgr565(uint8_t red, uint8_t green, uint8_t blue, uint16_t* bgr565) { - uint16_t _bgr565 = (blue >> 3); - _bgr565 = (_bgr565 << 6) | (green >> 2); - _bgr565 = (_bgr565 << 5) | (red >> 3); - *bgr565 = _bgr565; - } - - static void rgb565ToRgb888(uint16_t rgb565, uint32_t* rgb888) { - uint32_t _rgb565 = rgb565; - uint8_t b = (_rgb565 >> 8) & 0xF8; - uint8_t g = (_rgb565 >> 3) & 0xFC; - uint8_t r = (_rgb565 << 3) & 0xF8; - - uint8_t* r8p = reinterpret_cast(rgb888); - uint8_t* g8p = r8p + 1; - uint8_t* b8p = r8p + 2; - - *r8p = r | ((r >> 3) & 0x7); - *g8p = g | ((g >> 2) & 0x3); - *b8p = b | ((b >> 3) & 0x7); - } -}; \ No newline at end of file diff --git a/Apps/GraphicsDemo/main/Include/drivers/DisplayDriver.h b/Apps/GraphicsDemo/main/Include/drivers/DisplayDriver.h deleted file mode 100644 index 5e3916a..0000000 --- a/Apps/GraphicsDemo/main/Include/drivers/DisplayDriver.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include - -/** - * Wrapper for display_* device driver functions - */ -class DisplayDriver { - - struct Device* device; - -public: - - explicit DisplayDriver(struct Device* device) : device(device) { - device_get(device); - } - - ~DisplayDriver() { - device_put(device); - } - - bool lock(TickType_t timeout = tt::kernel::FREERTOS_MAX_TICKS) const { - return device_try_lock(device, timeout); - } - - void unlock() const { - device_unlock(device); - } - - uint16_t getWidth() const { - return display_get_resolution_x(device); - } - - uint16_t getHeight() const { - return display_get_resolution_y(device); - } - - enum DisplayColorFormat getColorFormat() const { - return display_get_color_format(device); - } - - void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const { - display_draw_bitmap(device, xStart, yStart, xEnd, yEnd, pixelData); - } -}; diff --git a/Apps/GraphicsDemo/main/Include/drivers/TouchDriver.h b/Apps/GraphicsDemo/main/Include/drivers/TouchDriver.h deleted file mode 100644 index 1b75369..0000000 --- a/Apps/GraphicsDemo/main/Include/drivers/TouchDriver.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include - -/** - * Wrapper for touch_* and mouse_* device driver functions. - * Either device can be absent. A press of the primary mouse button counts as a touch. - */ -class TouchDriver { - - struct Device* touchDevice; - struct Device* mouseDevice; - -public: - - TouchDriver(struct Device* touchDevice, struct Device* mouseDevice) : touchDevice(touchDevice), mouseDevice(mouseDevice) { - if (touchDevice != nullptr) { - device_get(touchDevice); - } - if (mouseDevice != nullptr) { - device_get(mouseDevice); - } - } - - ~TouchDriver() { - if (touchDevice != nullptr) { - device_put(touchDevice); - } - if (mouseDevice != nullptr) { - device_put(mouseDevice); - } - } - - bool isTouched() const { - if (touchDevice != nullptr) { - uint16_t x, y, strength; - uint8_t count = 0; - // Poll without blocking: perform one read attempt, then report whatever is cached. - touch_read_data(touchDevice, 0); - if (touch_get_touched_points(touchDevice, &x, &y, &strength, &count, 1)) { - return true; - } - } - bool pressed = false; - return mouseDevice != nullptr && mouse_get_button_pressed(mouseDevice, 0, &pressed) == ERROR_NONE && pressed; - } -}; diff --git a/Apps/GraphicsDemo/main/Source/Application.cpp b/Apps/GraphicsDemo/main/Source/Application.cpp index 5fa7370..56ca18f 100644 --- a/Apps/GraphicsDemo/main/Source/Application.cpp +++ b/Apps/GraphicsDemo/main/Source/Application.cpp @@ -1,70 +1,125 @@ #include "Application.h" -#include "PixelBuffer.h" -#include "esp_log.h" + +#include + +#include +#include #include +#include +#include + constexpr auto TAG = "Application"; -static bool isTouched(TouchDriver* touch) { - return touch->isTouched(); +static uint16_t toRgb565(uint8_t r, uint8_t g, uint8_t b) { + return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); } -void createRgbRow(PixelBuffer& buffer) { - uint8_t offset = buffer.getPixelWidth() / 3; - for (int i = 0; i < buffer.getPixelWidth(); ++i) { - if (i < offset) { - buffer.setPixel(i, 0, 255, 0, 0); - } else if (i < offset * 2) { - buffer.setPixel(i, 0, 0, 255, 0); - } else { - buffer.setPixel(i, 0, 0, 0, 255); - } +/** Three colour bands across the top half. */ +static void drawBandsArea(gfx::Surface& surface, int y, int height) { + const int width = surface.width(); + const int stripe = width / 3; + if (stripe <= 0) { + return; } + + // On a grayscale panel, equal-luminance colours (pure red/green/blue all average to 85) + // collapse into one flat block. Use explicit dark/mid/light greys there so the three bands stay + // visually distinct; colour displays still get real RGB. + const bool grayscale = surface.format() == DISPLAY_COLOR_FORMAT_GRAYSCALE8; + const uint16_t first = grayscale ? toRgb565(64, 64, 64) : toRgb565(255, 0, 0); + const uint16_t second = grayscale ? toRgb565(160, 160, 160) : toRgb565(0, 255, 0); + const uint16_t third = grayscale ? toRgb565(230, 230, 230) : toRgb565(0, 0, 255); + + surface.fillRect(0, y, stripe, height, first); + surface.fillRect(stripe, y, stripe, height, second); + surface.fillRect(stripe * 2, y, width - stripe * 2, height, third); } -void createRgbFadingRow(PixelBuffer& buffer) { - uint8_t stroke = buffer.getPixelWidth() / 3; - for (int i = 0; i < buffer.getPixelWidth(); ++i) { - if (i < stroke) { - auto color = i * 255 / stroke; - buffer.setPixel(i, 0, color, 0, 0); - } else if (i < stroke * 2) { - auto color = (i - stroke) * 255 / stroke; - buffer.setPixel(i, 0, 0, color, 0); +/** Red/green/blue gradients across the bottom half. */ +static void drawFadingArea(gfx::Surface& surface, int y, int height) { + const int width = surface.width(); + const int stroke = std::max(width / 3, 1); + + for (int x = 0; x < width; x++) { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + if (x < stroke) { + r = static_cast(x * 255 / stroke); + } else if (x < stroke * 2) { + g = static_cast((x - stroke) * 255 / stroke); } else { - auto color = (i - (2*stroke)) * 255 / stroke; - buffer.setPixel(i, 0, 0, 0, color); + b = static_cast((x - stroke * 2) * 255 / stroke); } + surface.fillRect(x, y, 1, height, toRgb565(r, g, b)); } } -void runApplication(DisplayDriver* display, TouchDriver* touch) { - // Single row buffers - PixelBuffer line_clear_buffer(display->getWidth(), 1, display->getColorFormat()); - line_clear_buffer.clear(); - PixelBuffer line_buffer(display->getWidth(), 1, display->getColorFormat()); - line_buffer.clear(); +static void drawPattern(gfx::Surface& surface, int display_height) { + // Clearing the surface also makes the callback overwrite every pixel it covers, so the target + // doesn't have to copy the previous frame forward (Prepare::Overwrite). + surface.clear(); + + const int half = display_height / 2; + drawBandsArea(surface, 0, half); + drawFadingArea(surface, half, display_height - half); +} + +void runApplication(gfx::Target* target, AnyInput* input) { + const gfx::Info& info = target->info(); + const bool slow_refresh = info.slowRefresh; + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + struct AppEventSubscription sub {}; + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + bool should_close = false; do { - // Draw row by row - // This is placed in a loop to test the SPI locking mechanismss - for (int i = 0; i < display->getHeight(); i++) { - - if (i == 0) { - createRgbRow(line_buffer); - } else if (i == display->getHeight() / 2) { - createRgbFadingRow(line_buffer); + target->render([height = info.height](gfx::Surface& surface) { + drawPattern(surface, height); + }, gfx::Prepare::Overwrite); + + if (slow_refresh) { + // Fast partial updates never fully resolve grayscale on this kind of panel - they + // flatten it towards black and white. One quality pass does. + target->refresh(); + } + + struct AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + should_close = true; } + } + if (should_close) { + break; + } - display->lock(); - display->drawBitmap(0, i, display->getWidth(), i + 1, line_buffer.getData()); - display->unlock(); + if (slow_refresh) { + // The content is static, and repeating it continuously would leave a slow panel + // flickering through partial and quality refreshes. Draw once and wait for input. + while (true) { + struct AppEvent pending {}; + while (app_event_poll(&sub, &pending) == ERROR_NONE) { + if (pending.type == APP_EVENT_CLOSE) { + should_close = true; + } + } + if (should_close || input->activity()) { + break; + } + tt::kernel::delayTicks(tt::kernel::millisToTicks(50)); + } + break; } - // Give other tasks space to breathe - // SPI displays would otherwise time out SPI SD card access + // Give other tasks space to breathe: SPI displays would otherwise time out SPI SD access. tt::kernel::delayTicks(1); - } while (!isTouched(touch)); -} + } while (!input->activity()); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); +} diff --git a/Apps/GraphicsDemo/main/Source/Main.cpp b/Apps/GraphicsDemo/main/Source/Main.cpp index 65334b9..9aca1b2 100644 --- a/Apps/GraphicsDemo/main/Source/Main.cpp +++ b/Apps/GraphicsDemo/main/Source/Main.cpp @@ -1,18 +1,18 @@ #include "Application.h" -#include "drivers/DisplayDriver.h" -#include "drivers/TouchDriver.h" #include #include -#include +#include #include +#include + +#include +#include #include #include #include -#include -#include #include #include #include @@ -25,7 +25,7 @@ constexpr auto TAG = "Main"; static void showErrorAndWait(AppInstanceId appInstanceId, const char* message) { const char* argv[] = { "Error", message, "OK" }; uint32_t dialogInstanceId = 0; - app_start_for_result("AlertDialog", 3, argv, appInstanceId, &dialogInstanceId); + app_start_for_result("tactility.alertdialog", 3, argv, appInstanceId, &dialogInstanceId); if (dialogInstanceId == 0) { return; } @@ -71,42 +71,48 @@ int main(int argc, char* argv[]) { return 0; } - struct Device* touch_device = nullptr; - struct Device* mouse_device = nullptr; - device_get_first_active_by_type(&TOUCH_TYPE, &touch_device); - device_get_first_active_by_type(&MOUSE_TYPE, &mouse_device); - if (touch_device == nullptr && mouse_device == nullptr) { - ESP_LOGE(TAG, "No touch device found"); - device_put(display_device); - showErrorAndWait(app_instance_id, "No touch device was found."); - return 0; - } - // Stop LVGL first (because it's currently using the drivers we want to use) module_stop(&lvgl_module); - ESP_LOGI(TAG, "Creating display driver"); - auto display = new DisplayDriver(display_device); + ESP_LOGI(TAG, "Creating display target"); + auto target = new gfx::Target(display_device); device_put(display_device); - ESP_LOGI(TAG, "Creating touch driver"); - auto touch = new TouchDriver(touch_device, mouse_device); - if (touch_device != nullptr) { - device_put(touch_device); + if (!target->valid()) { + ESP_LOGE(TAG, "Display doesn't support direct rendering"); + delete target; + + if (!module_is_started(&lvgl_module)) { + module_start(&lvgl_module); + } + + showErrorAndWait(app_instance_id, "This display can't be used for direct rendering."); + return 0; } - if (mouse_device != nullptr) { - device_put(mouse_device); + + auto input = new AnyInput(); + if (!input->available()) { + ESP_LOGE(TAG, "No input device found"); + delete input; + delete target; + + if (!module_is_started(&lvgl_module)) { + module_start(&lvgl_module); + } + + showErrorAndWait(app_instance_id, "No input device was found."); + return 0; } // Run the main logic ESP_LOGI(TAG, "Running application"); - runApplication(display, touch); + runApplication(target, input); - ESP_LOGI(TAG, "Cleanup display driver"); - delete display; + ESP_LOGI(TAG, "Cleanup display target"); + delete target; - ESP_LOGI(TAG, "Cleanup touch driver"); - delete touch; + ESP_LOGI(TAG, "Cleanup input"); + delete input; // Restart LVGL to resume rendering of regular apps if (!module_is_started(&lvgl_module)) { diff --git a/Apps/M5UnitTest/main/Source/M5UnitTest.cpp b/Apps/M5UnitTest/main/Source/M5UnitTest.cpp index 49a01e7..2090993 100644 --- a/Apps/M5UnitTest/main/Source/M5UnitTest.cpp +++ b/Apps/M5UnitTest/main/Source/M5UnitTest.cpp @@ -1,5 +1,6 @@ #include "M5UnitTest.h" #include "TestListView.h" +#include "UiScale.h" #include "TestUnit8Encoder.h" #include "TestUnitByteButton.h" #include "TestUnitJoystick2.h" @@ -66,12 +67,16 @@ void stopActiveTest(Context* ctx) { void m5UnitTestCreateWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(parent, uiBgColor(), 0); + lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(parent, 0, 0); ctx->wrapper = lv_obj_create(parent); lv_obj_set_width(ctx->wrapper, LV_PCT(100)); lv_obj_set_flex_grow(ctx->wrapper, 1); + lv_obj_set_scrollable(ctx->wrapper, false); lv_obj_set_layout(ctx->wrapper, LV_LAYOUT_FLEX); lv_obj_set_flex_flow(ctx->wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(ctx->wrapper, 0, 0); diff --git a/Apps/M5UnitTest/main/Source/TestListView.cpp b/Apps/M5UnitTest/main/Source/TestListView.cpp index ccafc00..f200227 100644 --- a/Apps/M5UnitTest/main/Source/TestListView.cpp +++ b/Apps/M5UnitTest/main/Source/TestListView.cpp @@ -49,24 +49,47 @@ void onBtnClicked(lv_event_t* e) { void testListViewCreate(lv_obj_t* parent, Context* app) { lvgl_toolbar_create(parent, "M5 Unit Test"); - lv_obj_t* list = lv_list_create(parent); + // Build the list from neutral widgets instead of lv_list. The Material + // theme's lv_list button style adds large margins/paddings that make the + // 11-entry list overflow on small and large displays alike. + lv_obj_t* list = lv_obj_create(parent); lv_obj_set_width(list, LV_PCT(100)); lv_obj_set_flex_grow(list, 1); + lv_obj_set_layout(list, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(list, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); lv_obj_set_style_pad_all(list, uiPad(), 0); lv_obj_set_style_pad_row(list, uiRowGap(), 0); - lv_obj_set_style_border_width(list, 0, 0); - lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, 0); + lv_obj_set_scrollable(list, true); + lv_obj_set_scroll_dir(list, LV_DIR_VER); const lv_font_t* font = lvgl_get_text_font(uiFont()); + const lv_font_t* iconFont = lvgl_get_shared_icon_default_font(); + const lv_coord_t rowH = uiH() < 200 ? 28 : 40; for (int i = 0; i < UNIT_COUNT; i++) { - lv_obj_t* btn = lv_list_add_button(list, UNIT_ICONS[i], UNIT_NAMES[i]); + lv_obj_t* btn = lv_button_create(list); + lv_obj_set_size(btn, LV_PCT(100), rowH); + lv_obj_set_layout(btn, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_hor(btn, 10, 0); + lv_obj_set_style_pad_ver(btn, 2, 0); + lv_obj_set_style_bg_color(btn, (i & 1) ? uiPanelColor() : uiBtnColor(), 0); lv_obj_set_user_data(btn, (void*)(intptr_t)i); lv_obj_add_event_cb(btn, onBtnClicked, LV_EVENT_CLICKED, app); - // lv_list_add_button creates: child 0 = icon label, child 1 = text label - lv_obj_t* textLbl = lv_obj_get_child(btn, 1); - if (textLbl) lv_obj_set_style_text_font(textLbl, font, 0); - lv_obj_t* iconLbl = lv_obj_get_child(btn, 0); - if (iconLbl) lv_obj_set_style_text_font(iconLbl, lvgl_get_shared_icon_default_font(), 0); + + lv_obj_t* iconLbl = lv_label_create(btn); + lv_label_set_text(iconLbl, UNIT_ICONS[i]); + lv_obj_set_style_text_font(iconLbl, iconFont, 0); + lv_obj_set_style_text_color(iconLbl, uiTextColor(), 0); + lv_obj_set_width(iconLbl, 28); + lv_obj_set_style_text_align(iconLbl, LV_TEXT_ALIGN_CENTER, 0); + + lv_obj_t* textLbl = lv_label_create(btn); + lv_label_set_text(textLbl, UNIT_NAMES[i]); + lv_obj_set_style_text_font(textLbl, font, 0); + lv_obj_set_style_text_color(textLbl, uiTextColor(), 0); + lv_obj_set_flex_grow(textLbl, 1); } } diff --git a/Apps/M5UnitTest/main/Source/TestUnit8Encoder.cpp b/Apps/M5UnitTest/main/Source/TestUnit8Encoder.cpp index d5acb96..68dadcd 100644 --- a/Apps/M5UnitTest/main/Source/TestUnit8Encoder.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnit8Encoder.cpp @@ -5,13 +5,14 @@ #include #include #include +#include #include namespace { void selectIfNeeded(TestUnit8Encoder* self) { - if (self->usingPaHub_ && self->hub_.isPresent()) - self->hub_.select(self->hub_.currentChannel()); + if (self->usingPaHub_ && self->hub_.isPresent() && self->encChannel_ != UnitPaHub::NO_CHANNEL) + self->hub_.select(self->encChannel_); } void update(TestUnit8Encoder* self) { @@ -56,6 +57,8 @@ void onTimer(lv_timer_t* t) { void testUnit8EncoderStart(TestUnit8Encoder* self, lv_obj_t* parent, Context* app) { self->app_ = app; + self->usingPaHub_ = false; + self->encChannel_ = UnitPaHub::NO_CHANNEL; memset(self->counters_, 0, sizeof(self->counters_)); memset(self->ledColors_, 0, sizeof(self->ledColors_)); @@ -69,7 +72,7 @@ void testUnit8EncoderStart(TestUnit8Encoder* self, lv_obj_t* parent, Context* ap const lv_font_t* fnt = lvgl_get_text_font(uiFont()); - // Status label shown when not connected — sits above the grid at full width + // Status label shown when not connected - sits above the grid at full width self->lblStatus_ = lv_label_create(parent); lv_obj_set_style_text_font(self->lblStatus_, fnt, 0); lv_obj_set_width(self->lblStatus_, LV_PCT(100)); @@ -99,7 +102,7 @@ void testUnit8EncoderStart(TestUnit8Encoder* self, lv_obj_t* parent, Context* ap lv_obj_set_style_pad_column(card, uiRowGap(), 0); lv_obj_set_style_bg_opa(card, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(card, 0, 0); - lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(card, false); return card; }; auto makeDot = [&](lv_obj_t* parent) -> lv_obj_t* { @@ -109,7 +112,7 @@ void testUnit8EncoderStart(TestUnit8Encoder* self, lv_obj_t* parent, Context* ap lv_obj_set_style_bg_color(dot, lv_color_hex(0x333333), 0); lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); lv_obj_set_style_border_width(dot, 0, 0); - lv_obj_remove_flag(dot, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(dot, false); return dot; }; @@ -148,20 +151,29 @@ void testUnit8EncoderStart(TestUnit8Encoder* self, lv_obj_t* parent, Context* ap return; } - if (self->enc_.begin(i2c)) { - self->usingPaHub_ = false; - } else if (self->hub_.begin(i2c)) { + if (self->hub_.begin(i2c)) { + // A PaHub occupies the Grove port, so every unit is behind it. Scan the + // hub before trying a direct address: a channel left selected by an + // earlier test would otherwise look like a directly-connected unit and + // then fail as soon as the mux is switched. self->usingPaHub_ = true; bool found = false; for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !found; ch++) { - self->hub_.select(ch); - if (self->enc_.begin(i2c)) found = true; + if (!self->hub_.select(ch)) continue; + ESP_LOGI("M5UnitTest", "Trying 8Encoder on PaHub CH%u", (unsigned)ch); + if (self->enc_.begin(i2c)) { + found = true; + self->encChannel_ = ch; + ESP_LOGI("M5UnitTest", "8Encoder found on PaHub CH%u", (unsigned)ch); + } } if (!found) { self->hub_.deselect(); lv_label_set_text(self->lblStatus_, "8Encoder not found"); return; } + } else if (self->enc_.begin(i2c)) { + self->usingPaHub_ = false; } else { lv_label_set_text(self->lblStatus_, "8Encoder not found"); return; @@ -176,6 +188,8 @@ void testUnit8EncoderStop(TestUnit8Encoder* self) { selectIfNeeded(self); if (self->enc_.isPresent()) self->enc_.setAllLeds(0x000000); if (self->usingPaHub_ && self->hub_.isPresent()) self->hub_.deselect(); + self->encChannel_ = UnitPaHub::NO_CHANNEL; + self->usingPaHub_ = false; self->lblStatus_ = nullptr; memset(self->lblCounters_, 0, sizeof(self->lblCounters_)); memset(self->dotButtons_, 0, sizeof(self->dotButtons_)); diff --git a/Apps/M5UnitTest/main/Source/TestUnit8Encoder.h b/Apps/M5UnitTest/main/Source/TestUnit8Encoder.h index b363289..309bffe 100644 --- a/Apps/M5UnitTest/main/Source/TestUnit8Encoder.h +++ b/Apps/M5UnitTest/main/Source/TestUnit8Encoder.h @@ -18,6 +18,7 @@ struct TestUnit8Encoder { int32_t counters_[8] = {}; uint32_t ledColors_[Unit8Encoder::LED_COUNT] = {}; bool usingPaHub_ = false; + uint8_t encChannel_ = UnitPaHub::NO_CHANNEL; }; void testUnit8EncoderStart(TestUnit8Encoder* self, lv_obj_t* parent, Context* app); diff --git a/Apps/M5UnitTest/main/Source/TestUnitByteButton.cpp b/Apps/M5UnitTest/main/Source/TestUnitByteButton.cpp index 6c6db79..3c78cc9 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitByteButton.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnitByteButton.cpp @@ -10,8 +10,10 @@ namespace { void selectIfNeeded(TestUnitByteButton* self) { - if (self->usingPaHub_ && self->hub_.isPresent()) - self->hub_.select(self->hub_.currentChannel()); + if (self->usingPaHub_ && self->hub_.isPresent() && + self->channel_ != UnitPaHub::NO_CHANNEL) { + self->hub_.select(self->channel_); + } } void update(TestUnitByteButton* self) { @@ -83,7 +85,7 @@ void testUnitByteButtonStart(TestUnitByteButton* self, lv_obj_t* parent, Context lv_obj_set_style_bg_opa(sq, LV_OPA_COVER, 0); lv_obj_set_style_border_color(sq, lv_color_hex(0x444444), 0); lv_obj_set_style_border_width(sq, 1, 0); - lv_obj_remove_flag(sq, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(sq, false); self->indicators_[i] = sq; } @@ -98,22 +100,30 @@ void testUnitByteButtonStart(TestUnitByteButton* self, lv_obj_t* parent, Context return; } - if (self->unit_.begin(i2c)) { - self->usingPaHub_ = false; - } else if (self->hub_.begin(i2c)) { - self->usingPaHub_ = true; - bool found = false; + bool found = false; + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + + // PaHub first. A channel left selected by an earlier test can make a + // direct probe succeed through the mux and then break on the next switch. + if (self->hub_.begin(i2c)) { for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !found; ch++) { - self->hub_.select(ch); - if (self->unit_.begin(i2c)) found = true; - } - if (!found) { - self->hub_.deselect(); - for (int i = 0; i < TestUnitByteButton::BTN_COUNT; i++) lv_obj_set_style_bg_color(self->indicators_[i], lv_color_hex(TestUnitByteButton::COLOR_ERROR), 0); - lv_label_set_text(hint, "ByteButton not found"); - return; + if (!self->hub_.select(ch)) continue; + if (self->unit_.begin(i2c)) { + self->usingPaHub_ = true; + self->channel_ = ch; + found = true; + } } - } else { + if (!found) self->hub_.deselect(); + } + if (!found && self->unit_.begin(i2c)) { + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + found = true; + } + + if (!found) { for (int i = 0; i < TestUnitByteButton::BTN_COUNT; i++) lv_obj_set_style_bg_color(self->indicators_[i], lv_color_hex(TestUnitByteButton::COLOR_ERROR), 0); lv_label_set_text(hint, "ByteButton not found"); return; @@ -130,5 +140,7 @@ void testUnitByteButtonStop(TestUnitByteButton* self) { for (int i = 0; i < TestUnitByteButton::BTN_COUNT; i++) self->unit_.setLed((uint8_t)i, 0x000000); } if (self->usingPaHub_ && self->hub_.isPresent()) self->hub_.deselect(); + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; memset(self->indicators_, 0, sizeof(self->indicators_)); } diff --git a/Apps/M5UnitTest/main/Source/TestUnitByteButton.h b/Apps/M5UnitTest/main/Source/TestUnitByteButton.h index 4c11033..550a34f 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitByteButton.h +++ b/Apps/M5UnitTest/main/Source/TestUnitByteButton.h @@ -20,6 +20,7 @@ struct TestUnitByteButton { uint32_t ledColors_[BTN_COUNT] = {}; bool prevPressed_[BTN_COUNT]= {}; bool usingPaHub_ = false; + uint8_t channel_ = UnitPaHub::NO_CHANNEL; }; void testUnitByteButtonStart(TestUnitByteButton* self, lv_obj_t* parent, Context* app); diff --git a/Apps/M5UnitTest/main/Source/TestUnitCardKB2.cpp b/Apps/M5UnitTest/main/Source/TestUnitCardKB2.cpp index 06301b0..ca427f4 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitCardKB2.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnitCardKB2.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace { @@ -88,6 +89,13 @@ void showConnectOverlay(TestUnitCardKB2* self) { self->connectOverlay_ = lv_obj_create(self->parentRef_); lv_obj_set_size(self->connectOverlay_, LV_PCT(100), LV_PCT(100)); lv_obj_set_pos(self->connectOverlay_, 0, 0); + // The overlay is a child of the flex-column wrapper; without IGNORE_LAYOUT + // the flex parent treats it as another row and pushes it below the banner. + lv_obj_add_flag(self->connectOverlay_, LV_OBJ_FLAG_IGNORE_LAYOUT); + lv_obj_set_style_bg_color(self->connectOverlay_, uiBgColor(), 0); + lv_obj_set_style_bg_opa(self->connectOverlay_, LV_OPA_COVER, 0); + lv_obj_set_style_radius(self->connectOverlay_, 0, 0); + lv_obj_set_style_border_width(self->connectOverlay_, 0, 0); lv_obj_set_layout(self->connectOverlay_, LV_LAYOUT_FLEX); lv_obj_set_flex_flow(self->connectOverlay_, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(self->connectOverlay_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); @@ -137,6 +145,12 @@ void buildMainUI(TestUnitCardKB2* self) { lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(cont, 0, 0); + self->lblStatus_ = lv_label_create(cont); + lv_obj_set_style_text_font(self->lblStatus_, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_obj_set_style_text_color(self->lblStatus_, lv_color_hex(0x9AA4B2), 0); + lv_label_set_text(self->lblStatus_, ""); + lv_obj_set_width(self->lblStatus_, LV_PCT(100)); + if (uiW() >= 200) buildGrid(self, cont); self->lblHistory_ = lv_label_create(cont); @@ -152,8 +166,10 @@ void buildMainUI(TestUnitCardKB2* self) { // --------------------------------------------------------------------------- void selectIfNeeded(TestUnitCardKB2* self) { - if (self->usingPaHub_ && self->hub_.isPresent()) - self->hub_.select(self->hub_.currentChannel()); + if (self->usingPaHub_ && self->hub_.isPresent() && + self->channel_ != UnitPaHub::NO_CHANNEL) { + self->hub_.select(self->channel_); + } } // --------------------------------------------------------------------------- @@ -221,21 +237,47 @@ void doConnectI2C(TestUnitCardKB2* self) { } bool ok = false; - if (self->unit_.begin(i2c)) { - self->usingPaHub_ = false; - ok = true; - } else if (self->hub_.begin(i2c)) { - self->usingPaHub_ = true; + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + + // PaHub first: a PaHub occupies the Grove port, and a channel left selected + // by an earlier test can make a direct probe look valid until the next test + // switches the mux. The 0xF1 version read keeps the final result honest. + if (self->hub_.begin(i2c)) { for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !ok; ch++) { - self->hub_.select(ch); - if (self->unit_.begin(i2c)) ok = true; + if (!self->hub_.select(ch)) continue; + if (self->unit_.begin(i2c)) { + self->usingPaHub_ = true; + self->channel_ = ch; + ok = true; + } } if (!ok) self->hub_.deselect(); } + if (!ok && self->unit_.begin(i2c)) { + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + ok = true; + } buildMainUI(self); - if (!ok) lv_label_set_text(self->lblHistory_, "CardKB2 not found"); - else self->timer_ = lv_timer_create(onTimer, 50, self); + if (!ok) { + lv_label_set_text(self->lblHistory_, "CardKB2 not found"); + lv_label_set_text(self->lblStatus_, "I2C 0x5F: no response"); + return; + } + + char status[64]; + if (self->usingPaHub_) { + snprintf(status, sizeof(status), "I2C 0x5F CH%u fw %u", + (unsigned)self->channel_, (unsigned)self->unit_.firmwareVersion()); + } else { + snprintf(status, sizeof(status), "I2C 0x5F direct fw %u", + (unsigned)self->unit_.firmwareVersion()); + } + lv_label_set_text(self->lblStatus_, status); + lv_label_set_text(self->lblHistory_, "Type on the keyboard..."); + self->timer_ = lv_timer_create(onTimer, 50, self); } void doConnectUart(TestUnitCardKB2* self) { @@ -246,12 +288,16 @@ void doConnectUart(TestUnitCardKB2* self) { buildMainUI(self); if (!uart) { lv_label_set_text(self->lblHistory_, "grove0_uart not found"); + lv_label_set_text(self->lblStatus_, "UART: no grove0_uart"); return; } if (!self->unit_.beginUart(uart)) { lv_label_set_text(self->lblHistory_, "UART open failed"); + lv_label_set_text(self->lblStatus_, "UART 115200: open failed"); return; } + lv_label_set_text(self->lblStatus_, "UART 115200 8N1"); + lv_label_set_text(self->lblHistory_, "Type on the keyboard..."); self->timer_ = lv_timer_create(onTimer, 50, self); } @@ -279,6 +325,9 @@ void testUnitCardKB2Stop(TestUnitCardKB2* self) { if (self->timer_) { lv_timer_delete(self->timer_); self->timer_ = nullptr; } if (self->usingPaHub_ && self->hub_.isPresent()) self->hub_.deselect(); self->unit_.end(); + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + self->lblStatus_ = nullptr; self->lblHistory_ = nullptr; self->connectOverlay_ = nullptr; self->activeBtn_ = nullptr; diff --git a/Apps/M5UnitTest/main/Source/TestUnitCardKB2.h b/Apps/M5UnitTest/main/Source/TestUnitCardKB2.h index ba518c0..d7a3eba 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitCardKB2.h +++ b/Apps/M5UnitTest/main/Source/TestUnitCardKB2.h @@ -20,11 +20,13 @@ struct TestUnitCardKB2 { UnitCardKB2 unit_; lv_timer_t* timer_ = nullptr; bool usingPaHub_ = false; + uint8_t channel_ = UnitPaHub::NO_CHANNEL; // Connection selection overlay (shown before connecting) lv_obj_t* connectOverlay_ = nullptr; // Main content (shown after connecting) + lv_obj_t* lblStatus_ = nullptr; lv_obj_t* lblHistory_ = nullptr; char history_[64] = {}; uint8_t histLen_ = 0; diff --git a/Apps/M5UnitTest/main/Source/TestUnitJoystick2.cpp b/Apps/M5UnitTest/main/Source/TestUnitJoystick2.cpp index 26f6a1a..416fed6 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitJoystick2.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnitJoystick2.cpp @@ -11,8 +11,10 @@ namespace { void selectIfNeeded(TestUnitJoystick2* self) { - if (self->usingPaHub_ && self->hub_.isPresent()) - self->hub_.select(self->hub_.currentChannel()); + if (self->usingPaHub_ && self->hub_.isPresent() && + self->channel_ != UnitPaHub::NO_CHANNEL) { + self->hub_.select(self->channel_); + } } void update(TestUnitJoystick2* self) { @@ -102,15 +104,18 @@ void testUnitJoystick2Start(TestUnitJoystick2* self, lv_obj_t* parent, Context* self->joyCont_ = lv_obj_create(cont); lv_obj_set_size(self->joyCont_, JOY_AREA, JOY_AREA); lv_obj_set_style_bg_color(self->joyCont_, lv_color_hex(0x222222), 0); + lv_obj_set_style_bg_opa(self->joyCont_, LV_OPA_COVER, 0); lv_obj_set_style_radius(self->joyCont_, JOY_AREA / 2, 0); lv_obj_set_style_border_width(self->joyCont_, 2, 0); + lv_obj_set_style_border_color(self->joyCont_, lv_color_hex(0x666666), 0); lv_obj_set_style_pad_all(self->joyCont_, 0, 0); - lv_obj_remove_flag(self->joyCont_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(self->joyCont_, false); self->dot_ = lv_obj_create(self->joyCont_); lv_obj_set_size(self->dot_, DOT_SIZE, DOT_SIZE); lv_obj_set_style_radius(self->dot_, DOT_SIZE / 2, 0); lv_obj_set_style_bg_color(self->dot_, lv_color_hex(0x00FF00), 0); + lv_obj_set_style_bg_opa(self->dot_, LV_OPA_COVER, 0); lv_obj_set_style_border_width(self->dot_, 0, 0); lv_obj_set_pos(self->dot_, (JOY_AREA - DOT_SIZE) / 2, (JOY_AREA - DOT_SIZE) / 2); @@ -120,21 +125,30 @@ void testUnitJoystick2Start(TestUnitJoystick2* self, lv_obj_t* parent, Context* return; } - if (self->unit_.begin(i2c)) { - self->usingPaHub_ = false; - } else if (self->hub_.begin(i2c)) { - self->usingPaHub_ = true; - bool found = false; + bool found = false; + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + + // PaHub first. A channel left selected by an earlier test can make a + // direct probe succeed through the mux and then break on the next switch. + if (self->hub_.begin(i2c)) { for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !found; ch++) { - self->hub_.select(ch); - if (self->unit_.begin(i2c)) found = true; + if (!self->hub_.select(ch)) continue; + if (self->unit_.begin(i2c)) { + self->usingPaHub_ = true; + self->channel_ = ch; + found = true; + } } - if (!found) { - self->hub_.deselect(); - lv_label_set_text(self->lblXY_, "Joystick2 not found"); - return; - } - } else { + if (!found) self->hub_.deselect(); + } + if (!found && self->unit_.begin(i2c)) { + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + found = true; + } + + if (!found) { lv_label_set_text(self->lblXY_, "Joystick2 not found"); return; } @@ -148,5 +162,7 @@ void testUnitJoystick2Stop(TestUnitJoystick2* self) { selectIfNeeded(self); if (self->unit_.isPresent()) self->unit_.setLed(0x000000); if (self->usingPaHub_ && self->hub_.isPresent()) self->hub_.deselect(); + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; self->lblXY_ = self->lblButton_ = self->dot_ = self->joyCont_ = nullptr; } diff --git a/Apps/M5UnitTest/main/Source/TestUnitJoystick2.h b/Apps/M5UnitTest/main/Source/TestUnitJoystick2.h index 18e3bf4..c26c61e 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitJoystick2.h +++ b/Apps/M5UnitTest/main/Source/TestUnitJoystick2.h @@ -15,6 +15,7 @@ struct TestUnitJoystick2 { lv_obj_t* joyCont_ = nullptr; lv_timer_t* timer_ = nullptr; bool usingPaHub_ = false; + uint8_t channel_ = UnitPaHub::NO_CHANNEL; int joyArea_ = 120; int dotSize_ = 16; }; diff --git a/Apps/M5UnitTest/main/Source/TestUnitLcd.cpp b/Apps/M5UnitTest/main/Source/TestUnitLcd.cpp index c543df7..c6c9f89 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitLcd.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnitLcd.cpp @@ -4,11 +4,12 @@ #include "UiScale.h" #include #include +#include namespace { void selectIfNeeded(TestUnitLcd* self) { - if (self->usingPaHub_ && self->hub_.isPresent()) + if (self->usingPaHub_ && self->hub_.isPresent() && self->lcdChannel_ != UnitPaHub::NO_CHANNEL) self->hub_.select(self->lcdChannel_); } @@ -164,21 +165,29 @@ void testUnitLcdStart(TestUnitLcd* self, lv_obj_t* parent, Context* app) { return; } - // Try standalone first, then scan PaHub channels - if (self->lcd_.begin(i2c)) { - self->usingPaHub_ = false; - } else if (self->hub_.begin(i2c)) { + // A PaHub occupies the Grove port, so every unit is behind it. Scan the hub + // before trying a direct address: a channel left selected by an earlier test + // would otherwise look like a directly-connected unit and then fail as soon + // as the mux is switched. + if (self->hub_.begin(i2c)) { self->usingPaHub_ = true; bool found = false; for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !found; ch++) { - self->hub_.select(ch); - if (self->lcd_.begin(i2c)) { found = true; self->lcdChannel_ = ch; } + if (!self->hub_.select(ch)) continue; + ESP_LOGI("M5UnitTest", "Trying LCD on PaHub CH%u", (unsigned)ch); + if (self->lcd_.begin(i2c)) { + found = true; + self->lcdChannel_ = ch; + ESP_LOGI("M5UnitTest", "LCD found on PaHub CH%u", (unsigned)ch); + } } if (!found) { self->hub_.deselect(); lv_label_set_text(self->lblStatus_, "LCD Unit not found"); return; } + } else if (self->lcd_.begin(i2c)) { + self->usingPaHub_ = false; } else { lv_label_set_text(self->lblStatus_, "LCD Unit not found"); return; @@ -194,5 +203,7 @@ void testUnitLcdStop(TestUnitLcd* self) { selectIfNeeded(self); if (self->lcd_.isPresent()) self->lcd_.setBrightness(0); if (self->usingPaHub_ && self->hub_.isPresent()) self->hub_.deselect(); + self->lcdChannel_ = UnitPaHub::NO_CHANNEL; + self->usingPaHub_ = false; self->lblStatus_ = self->sliderBr_ = self->lblRotation_ = nullptr; } diff --git a/Apps/M5UnitTest/main/Source/TestUnitLcd.h b/Apps/M5UnitTest/main/Source/TestUnitLcd.h index ca6086d..c578856 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitLcd.h +++ b/Apps/M5UnitTest/main/Source/TestUnitLcd.h @@ -14,7 +14,7 @@ struct TestUnitLcd { lv_obj_t* lblRotation_ = nullptr; uint8_t rotation_ = 0; bool usingPaHub_ = false; - uint8_t lcdChannel_ = 0; + uint8_t lcdChannel_ = UnitPaHub::NO_CHANNEL; }; void testUnitLcdStart(TestUnitLcd* self, lv_obj_t* parent, Context* app); diff --git a/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.cpp b/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.cpp index 6507da3..d420ff1 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.cpp @@ -221,8 +221,10 @@ uint32_t testRoundRects(TestUnitLcdGfx* self) { // --------------------------------------------------------------------------- void selectIfNeeded(TestUnitLcdGfx* self) { - if (self->usingPaHub_ && self->hub_.isPresent()) - self->hub_.select(self->hub_.currentChannel()); + if (self->usingPaHub_ && self->hub_.isPresent() && + self->channel_ != UnitPaHub::NO_CHANNEL) { + self->hub_.select(self->channel_); + } } void appendLog(TestUnitLcdGfx* self, const char* name, uint32_t us) { @@ -392,21 +394,30 @@ void testUnitLcdGfxStart(TestUnitLcdGfx* self, lv_obj_t* parent, Context* app) { Device* i2c = findGroveI2cDevice(); if (!i2c) { lv_label_set_text(self->lblPhase_, "grove0_i2c not found"); return; } - if (self->lcd_.begin(i2c)) { - self->usingPaHub_ = false; - } else if (self->hub_.begin(i2c)) { - self->usingPaHub_ = true; - bool found = false; + bool found = false; + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + + // PaHub first. A channel left selected by an earlier test can make a + // direct probe succeed through the mux and then break on the next switch. + if (self->hub_.begin(i2c)) { for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !found; ch++) { - self->hub_.select(ch); - if (self->lcd_.begin(i2c)) found = true; + if (!self->hub_.select(ch)) continue; + if (self->lcd_.begin(i2c)) { + self->usingPaHub_ = true; + self->channel_ = ch; + found = true; + } } - if (!found) { - self->hub_.deselect(); - lv_label_set_text(self->lblPhase_, "LCD not found"); - return; - } - } else { + if (!found) self->hub_.deselect(); + } + if (!found && self->lcd_.begin(i2c)) { + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + found = true; + } + + if (!found) { lv_label_set_text(self->lblPhase_, "LCD not found"); return; } @@ -436,5 +447,7 @@ void testUnitLcdGfxStop(TestUnitLcdGfx* self) { selectIfNeeded(self); if (self->lcd_.isPresent()) self->lcd_.setBrightness(0); if (self->usingPaHub_ && self->hub_.isPresent()) self->hub_.deselect(); + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; self->lblPhase_ = self->lblLog_ = nullptr; } diff --git a/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.h b/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.h index 5876740..940574d 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.h +++ b/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.h @@ -15,6 +15,7 @@ struct TestUnitLcdGfx { UnitPaHub hub_; UnitLcd lcd_; bool usingPaHub_ = false; + uint8_t channel_ = UnitPaHub::NO_CHANNEL; lv_obj_t* lblPhase_ = nullptr; lv_obj_t* lblLog_ = nullptr; diff --git a/Apps/M5UnitTest/main/Source/TestUnitPaHub.cpp b/Apps/M5UnitTest/main/Source/TestUnitPaHub.cpp index ace6fb5..c00f8c6 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitPaHub.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnitPaHub.cpp @@ -3,18 +3,47 @@ #include "GroveLookup.h" #include "UiScale.h" #include -#include -#include #include +#include #include #include namespace { -void probeSelected(TestUnitPaHub* self) { +// All known addresses now use an identity read: firmware version, LCD ID, or +// the MFRC522 VersionReg. The old write-only probes for 0x28/0x5F could ACK on +// an empty mux channel, so they are no longer treated as present. +bool isIdentityVerified(uint8_t addr) { + switch (addr) { + case 0x41: // 8Encoder: firmware version + case 0x3E: // LCD: 4-byte ID + case 0x40: // Scroll: firmware version + case 0x47: // ByteButton: firmware version + case 0x63: // Joystick2: firmware version + case 0x28: // RFID2: MFRC522/WS1850S VersionReg + case 0x5F: // CardKB2: firmware version + return true; + default: + return false; + } +} + +void probeSelected(TestUnitPaHub* self, bool logResults) { if (!self->hub_.isPresent() || self->selChannel_ < 0 || self->selChannel_ >= TestUnitPaHub::CH_COUNT) return; - self->hub_.select((uint8_t)self->selChannel_); + if (!self->hub_.select((uint8_t)self->selChannel_)) { + lv_label_set_text_fmt(self->lblCh_[self->selChannel_], "CH%d: select failed", self->selChannel_); + lv_label_set_text_fmt(self->lblStatus_, "Probed CH%d", self->selChannel_); + self->hub_.deselect(); + return; + } + + // Some downstream STM32/ESP32 units need longer than the mux settling time + // before their I2C listener answers again. A readback of the mux register + // here can also be blocked by downstream clock stretching and trigger a bus + // reset, which then makes the identity probes fail. Let the channel settle + // and go straight to the identity reads. + vTaskDelay(pdMS_TO_TICKS(20)); // Probe known unit addresses only (full-range scan can wedge the ESP-IDF i2c_master bus FSM) Device* i2c = findGroveI2cDevice(); @@ -23,23 +52,67 @@ void probeSelected(TestUnitPaHub* self) { self->hub_.deselect(); return; } - char found[256] = "Found: "; - bool any = false; - for (uint8_t addr : KNOWN_UNIT_ADDRS) { - if (i2c_controller_has_device_at_address(i2c, addr, - pdMS_TO_TICKS(10)) == ERROR_NONE) { - size_t remaining = sizeof(found) - strlen(found) - 1; - if (remaining < 7) { - strncat(found, "...", remaining); - break; + + auto scanAddresses = [&](char* found, bool& any) { + strcpy(found, "Found: "); + any = false; + const uint8_t channel = (uint8_t)self->selChannel_; + for (uint8_t addr : KNOWN_UNIT_ADDRS) { + bool present = unitProbe(i2c, addr); + if (!present) { + // An absent address can leave the controller's bus state + // unsettled. Reconnect the mux and give the real device one + // more clean chance before deciding the address is empty. + self->hub_.deselect(); + vTaskDelay(pdMS_TO_TICKS(5)); + if (self->hub_.select(channel)) { + vTaskDelay(pdMS_TO_TICKS(10)); + present = unitProbe(i2c, addr); + } + } + const bool verified = present && isIdentityVerified(addr); + if (logResults) { + ESP_LOGI("M5UnitTest", "PaHub CH%d 0x%02X: %s", + (int)self->selChannel_, (unsigned)addr, + verified ? "ACK (verified)" : "no"); } - char hex[8]; - snprintf(hex, sizeof(hex), "0x%02X ", addr); - strncat(found, hex, remaining); - any = true; + if (verified) { + size_t remaining = 256 - strlen(found) - 1; + if (remaining < 7) { + strncat(found, "...", remaining); + break; + } + char hex[8]; + snprintf(hex, sizeof(hex), "0x%02X ", addr); + strncat(found, hex, remaining); + any = true; + } + } + if (!any) strcpy(found, "No devices found"); + }; + + char found[256]; + bool any = false; + scanAddresses(found, any); + + // A failed probe can leave the controller's bus state unsettled. On a + // complete miss, disconnect and re-select the channel, then try once more. + // This is deliberately limited to the miss case so a populated channel + // stays fast. + if (!any) { + ESP_LOGI("M5UnitTest", "PaHub CH%d: no devices, retrying after mux reset", + (int)self->selChannel_); + self->hub_.deselect(); + vTaskDelay(pdMS_TO_TICKS(20)); + if (self->hub_.select((uint8_t)self->selChannel_)) { + vTaskDelay(pdMS_TO_TICKS(20)); + scanAddresses(found, any); } } - if (!any) strcpy(found, "No devices found"); + + if (logResults) { + ESP_LOGI("M5UnitTest", "PaHub CH%d result: %s", (int)self->selChannel_, found); + } lv_label_set_text_fmt(self->lblCh_[self->selChannel_], "CH%d: %s", self->selChannel_, found); lv_label_set_text_fmt(self->lblStatus_, "Probed CH%d", self->selChannel_); @@ -51,13 +124,8 @@ void onChannelBtn(lv_event_t* e) { auto* self = static_cast(lv_event_get_user_data(e)); int ch = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target_obj(e)); self->selChannel_ = ch; - probeSelected(self); -} - -void onTimer(lv_timer_t* t) { - auto* self = static_cast(lv_timer_get_user_data(t)); - if (window_manager_get_state(self->app_->window) != WINDOW_STATE_GRANTED) return; - if (self->selChannel_ >= 0) probeSelected(self); + ESP_LOGI("M5UnitTest", "Probing PaHub CH%d", ch); + probeSelected(self, true); } } // namespace @@ -82,7 +150,7 @@ void testUnitPaHubStart(TestUnitPaHub* self, lv_obj_t* parent, Context* app) { int pad = uiPad(); int gap = uiRowGap(); - // Row of channel buttons — flex-grow so they share space evenly at any width + // Row of channel buttons - flex-grow so they share space evenly at any width lv_obj_t* btnRow = lv_obj_create(cont); lv_obj_set_width(btnRow, LV_PCT(100)); lv_obj_set_height(btnRow, LV_SIZE_CONTENT); @@ -110,11 +178,15 @@ void testUnitPaHubStart(TestUnitPaHub* self, lv_obj_t* parent, Context* app) { self->lblStatus_ = lv_label_create(cont); lv_obj_set_style_text_font(self->lblStatus_, fnt, 0); + lv_obj_set_width(self->lblStatus_, LV_PCT(100)); + lv_label_set_long_mode(self->lblStatus_, LV_LABEL_LONG_WRAP); lv_label_set_text(self->lblStatus_, "Select a channel to probe"); for (int i = 0; i < TestUnitPaHub::CH_COUNT; i++) { self->lblCh_[i] = lv_label_create(cont); lv_obj_set_style_text_font(self->lblCh_[i], fnt, 0); + lv_obj_set_width(self->lblCh_[i], LV_PCT(100)); + lv_label_set_long_mode(self->lblCh_[i], LV_LABEL_LONG_WRAP); lv_label_set_text_fmt(self->lblCh_[i], "CH%d: -", i); } @@ -125,12 +197,9 @@ void testUnitPaHubStart(TestUnitPaHub* self, lv_obj_t* parent, Context* app) { } lv_label_set_text(self->lblStatus_, "PaHub ready - tap channel to probe"); - - self->timer_ = lv_timer_create(onTimer, 1000, self); } void testUnitPaHubStop(TestUnitPaHub* self) { - if (self->timer_) { lv_timer_delete(self->timer_); self->timer_ = nullptr; } if (self->hub_.isPresent()) self->hub_.deselect(); self->lblStatus_ = nullptr; for (int i = 0; i < TestUnitPaHub::CH_COUNT; i++) { self->btnCh_[i] = nullptr; self->lblCh_[i] = nullptr; } diff --git a/Apps/M5UnitTest/main/Source/TestUnitPaHub.h b/Apps/M5UnitTest/main/Source/TestUnitPaHub.h index 3fbfe3e..22d32f0 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitPaHub.h +++ b/Apps/M5UnitTest/main/Source/TestUnitPaHub.h @@ -12,7 +12,6 @@ struct TestUnitPaHub { lv_obj_t* lblStatus_ = nullptr; lv_obj_t* btnCh_[CH_COUNT] = {}; lv_obj_t* lblCh_[CH_COUNT] = {}; - lv_timer_t* timer_ = nullptr; int selChannel_ = -1; }; diff --git a/Apps/M5UnitTest/main/Source/TestUnitRfid2.cpp b/Apps/M5UnitTest/main/Source/TestUnitRfid2.cpp index f18a4f3..b55adcd 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitRfid2.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnitRfid2.cpp @@ -12,8 +12,10 @@ namespace { void selectIfNeeded(TestUnitRfid2* self) { - if (self->usingPaHub_ && self->hub_.isPresent()) - self->hub_.select(self->hub_.currentChannel()); + if (self->usingPaHub_ && self->hub_.isPresent() && + self->channel_ != UnitPaHub::NO_CHANNEL) { + self->hub_.select(self->channel_); + } } void showCard(TestUnitRfid2* self, const UnitRfid2::Uid& uid) { @@ -38,8 +40,8 @@ void showCard(TestUnitRfid2* self, const UnitRfid2::Uid& uid) { uid.sak, uid.atqa[0], uid.atqa[1]); lv_label_set_text(self->lblSak_, sakBuf); - lv_obj_add_flag(self->idleGroup_, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(self->cardGroup_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(self->idleGroup_, true); + lv_obj_set_hidden(self->cardGroup_, false); self->unit_.haltCard(); } @@ -75,8 +77,8 @@ void onPulseTimer(lv_timer_t* t) { void onClear(lv_event_t* e) { auto* self = static_cast(lv_event_get_user_data(e)); self->cardShown_ = false; - lv_obj_remove_flag(self->idleGroup_, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(self->cardGroup_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(self->idleGroup_, false); + lv_obj_set_hidden(self->cardGroup_, true); } } // namespace @@ -129,13 +131,18 @@ void testUnitRfid2Start(TestUnitRfid2* self, lv_obj_t* parent, Context* app) { lv_obj_set_style_bg_color(self->circle_, LV_COLOR_MAKE(0x20, 0xC0, 0x50), 0); lv_obj_set_style_bg_opa(self->circle_, self->pulseOpa_, 0); lv_obj_set_style_border_width(self->circle_, 0, 0); - lv_obj_remove_flag(self->circle_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(self->circle_, false); // "Tap a tag/card..." label lv_obj_t* tapLabel = lv_label_create(self->idleGroup_); lv_obj_set_style_text_font(tapLabel, lvgl_get_text_font(font), 0); lv_label_set_text(tapLabel, "Tap a tag/card..."); + self->lblReader_ = lv_label_create(self->idleGroup_); + lv_obj_set_style_text_font(self->lblReader_, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_obj_set_style_text_color(self->lblReader_, lv_color_hex(0x9AA4B2), 0); + lv_label_set_text(self->lblReader_, ""); + // ── Card info group ─────────────────────────────────────────────────────── self->cardGroup_ = lv_obj_create(content); lv_obj_set_width(self->cardGroup_, LV_PCT(100)); @@ -147,7 +154,7 @@ void testUnitRfid2Start(TestUnitRfid2* self, lv_obj_t* parent, Context* app) { lv_obj_set_style_pad_row(self->cardGroup_, rowGap, 0); lv_obj_set_style_bg_opa(self->cardGroup_, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(self->cardGroup_, 0, 0); - lv_obj_add_flag(self->cardGroup_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(self->cardGroup_, true); self->lblUid_ = lv_label_create(self->cardGroup_); lv_obj_set_style_text_font(self->lblUid_, @@ -172,25 +179,50 @@ void testUnitRfid2Start(TestUnitRfid2* self, lv_obj_t* parent, Context* app) { // ── Device discovery ───────────────────────────────────────────────────── Device* i2c = findGroveI2cDevice(); - if (!i2c) return; + if (!i2c) { + lv_label_set_text(self->lblReader_, "No I2C bus"); + return; + } - if (self->unit_.begin(i2c)) { - self->usingPaHub_ = false; - } else if (self->hub_.begin(i2c)) { - self->usingPaHub_ = true; - bool found = false; + bool found = false; + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + + // PaHub first. A PaHub occupies the Grove port, and a channel left selected + // by an earlier test can make a direct probe look valid until the next test + // switches the mux. The VersionReg read keeps the final result honest. + if (self->hub_.begin(i2c)) { for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !found; ch++) { - self->hub_.select(ch); - if (self->unit_.begin(i2c)) found = true; + if (!self->hub_.select(ch)) continue; + if (self->unit_.begin(i2c)) { + self->usingPaHub_ = true; + self->channel_ = ch; + found = true; + } } - if (!found) { - self->hub_.deselect(); - return; - } - } else { + if (!found) self->hub_.deselect(); + } + if (!found && self->unit_.begin(i2c)) { + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + found = true; + } + + if (!found) { + lv_label_set_text(self->lblReader_, "I2C 0x28: no response"); return; } + char status[80]; + if (self->usingPaHub_) { + snprintf(status, sizeof(status), "I2C 0x28 CH%u fw 0x%02X", + (unsigned)self->channel_, (unsigned)self->unit_.firmwareVersion()); + } else { + snprintf(status, sizeof(status), "I2C 0x28 direct fw 0x%02X", + (unsigned)self->unit_.firmwareVersion()); + } + lv_label_set_text(self->lblReader_, status); + self->timer_ = lv_timer_create(onTimer, 100, self); self->pulseTimer_ = lv_timer_create(onPulseTimer, 60, self); } @@ -200,7 +232,9 @@ void testUnitRfid2Stop(TestUnitRfid2* self) { if (self->pulseTimer_) { lv_timer_delete(self->pulseTimer_); self->pulseTimer_ = nullptr; } if (self->usingPaHub_ && self->hub_.isPresent()) self->hub_.deselect(); - self->cardShown_ = false; - self->idleGroup_ = self->circle_ = nullptr; - self->cardGroup_ = self->lblUid_ = self->lblType_ = self->lblSak_ = nullptr; + self->cardShown_ = false; + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + self->idleGroup_ = self->circle_ = self->lblReader_ = nullptr; + self->cardGroup_ = self->lblUid_ = self->lblType_ = self->lblSak_ = nullptr; } diff --git a/Apps/M5UnitTest/main/Source/TestUnitRfid2.h b/Apps/M5UnitTest/main/Source/TestUnitRfid2.h index 5854afa..51aaa07 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitRfid2.h +++ b/Apps/M5UnitTest/main/Source/TestUnitRfid2.h @@ -13,10 +13,12 @@ struct TestUnitRfid2 { lv_timer_t* pulseTimer_ = nullptr; bool usingPaHub_ = false; bool cardShown_ = false; + uint8_t channel_ = UnitPaHub::NO_CHANNEL; // Idle group lv_obj_t* idleGroup_ = nullptr; lv_obj_t* circle_ = nullptr; + lv_obj_t* lblReader_ = nullptr; // Card info group lv_obj_t* cardGroup_ = nullptr; diff --git a/Apps/M5UnitTest/main/Source/TestUnitScroll.cpp b/Apps/M5UnitTest/main/Source/TestUnitScroll.cpp index d3f4e37..069752c 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitScroll.cpp +++ b/Apps/M5UnitTest/main/Source/TestUnitScroll.cpp @@ -9,8 +9,10 @@ namespace { void selectIfNeeded(TestUnitScroll* self) { - if (self->usingPaHub_ && self->hub_.isPresent()) - self->hub_.select(self->hub_.currentChannel()); + if (self->usingPaHub_ && self->hub_.isPresent() && + self->channel_ != UnitPaHub::NO_CHANNEL) { + self->hub_.select(self->channel_); + } } void update(TestUnitScroll* self) { @@ -104,21 +106,31 @@ void testUnitScrollStart(TestUnitScroll* self, lv_obj_t* parent, Context* app) { return; } - if (self->unit_.begin(i2c)) { - self->usingPaHub_ = false; - } else if (self->hub_.begin(i2c)) { - self->usingPaHub_ = true; - bool found = false; + bool found = false; + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + + // PaHub first. If an earlier test left a channel selected, a direct probe + // can latch onto the unit through the mux but then lose it when the next + // test changes channels. + if (self->hub_.begin(i2c)) { for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !found; ch++) { - self->hub_.select(ch); - if (self->unit_.begin(i2c)) found = true; - } - if (!found) { - self->hub_.deselect(); - lv_label_set_text(self->lblCounter_, "Scroll not found"); - return; + if (!self->hub_.select(ch)) continue; + if (self->unit_.begin(i2c)) { + self->usingPaHub_ = true; + self->channel_ = ch; + found = true; + } } - } else { + if (!found) self->hub_.deselect(); + } + if (!found && self->unit_.begin(i2c)) { + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; + found = true; + } + + if (!found) { lv_label_set_text(self->lblCounter_, "Scroll not found"); return; } @@ -132,6 +144,8 @@ void testUnitScrollStop(TestUnitScroll* self) { selectIfNeeded(self); if (self->unit_.isPresent()) self->unit_.setLed(0x000000); if (self->usingPaHub_ && self->hub_.isPresent()) self->hub_.deselect(); + self->usingPaHub_ = false; + self->channel_ = UnitPaHub::NO_CHANNEL; self->lblCounter_ = self->lblButton_ = self->lblLed_ = nullptr; self->sliderR_ = self->sliderG_ = self->sliderB_ = nullptr; } diff --git a/Apps/M5UnitTest/main/Source/TestUnitScroll.h b/Apps/M5UnitTest/main/Source/TestUnitScroll.h index 7bb5a97..f7661b2 100644 --- a/Apps/M5UnitTest/main/Source/TestUnitScroll.h +++ b/Apps/M5UnitTest/main/Source/TestUnitScroll.h @@ -18,6 +18,7 @@ struct TestUnitScroll { lv_timer_t* timer_ = nullptr; int32_t counter_ = 0; bool usingPaHub_ = false; + uint8_t channel_ = UnitPaHub::NO_CHANNEL; }; void testUnitScrollStart(TestUnitScroll* self, lv_obj_t* parent, Context* app); diff --git a/Apps/M5UnitTest/main/Source/TestViewBase.cpp b/Apps/M5UnitTest/main/Source/TestViewBase.cpp index 0949fcb..18998d1 100644 --- a/Apps/M5UnitTest/main/Source/TestViewBase.cpp +++ b/Apps/M5UnitTest/main/Source/TestViewBase.cpp @@ -18,7 +18,7 @@ lv_obj_t* testViewCreateToolbar(lv_obj_t* parent, Context* app, const char* titl void testViewCreateBanner(lv_obj_t* parent, const char* unitName, const char* ifaceBadge, lv_color_t accentColor) { - lv_coord_t bannerH = uiH() < 200 ? 16 : 22; + lv_coord_t bannerH = uiH() < 200 ? 16 : (uiW() >= UI_LARGE_THRESHOLD ? 32 : 24); lv_obj_t* banner = lv_obj_create(parent); lv_obj_set_width(banner, LV_PCT(100)); @@ -32,7 +32,7 @@ void testViewCreateBanner(lv_obj_t* parent, const char* unitName, lv_obj_set_layout(banner, LV_LAYOUT_FLEX); lv_obj_set_flex_flow(banner, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(banner, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_remove_flag(banner, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(banner, false); // Unit name label (left-aligned) lv_obj_t* nameLabel = lv_label_create(banner); @@ -52,7 +52,7 @@ void testViewCreateBanner(lv_obj_t* parent, const char* unitName, lv_obj_set_style_radius(pill, 4, 0); lv_obj_set_style_pad_hor(pill, 4, 0); lv_obj_set_style_pad_ver(pill, 0, 0); - lv_obj_remove_flag(pill, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(pill, false); lv_obj_t* badgeLabel = lv_label_create(pill); lv_label_set_text(badgeLabel, ifaceBadge); diff --git a/Apps/M5UnitTest/main/Source/UiKit.h b/Apps/M5UnitTest/main/Source/UiKit.h new file mode 100644 index 0000000..1bca592 --- /dev/null +++ b/Apps/M5UnitTest/main/Source/UiKit.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +// --------------------------------------------------------------------------- +// M5UnitTest UI kit +// +// The global LVGL theme (Material) applies focus-ring margins, outline/shadow, +// large paddings and pill radii to every object/button. These test views were +// written against the stock widget defaults, so every widget is created +// through the helpers below. They reset the theme properties first; a view can +// still override any of them afterwards for its own layout. +// +// The macros at the bottom keep the existing view code readable: calls to +// lv_obj_create()/lv_button_create()/lv_label_create() transparently go through +// the neutral helpers. +// --------------------------------------------------------------------------- + +static inline lv_color_t uiBgColor() { return lv_color_hex(0x101418); } +static inline lv_color_t uiPanelColor(){ return lv_color_hex(0x1A2028); } +static inline lv_color_t uiBtnColor() { return lv_color_hex(0x2F3237); } +static inline lv_color_t uiTextColor() { return lv_color_white(); } + +static inline void uiResetWidget(lv_obj_t* obj) { + lv_obj_set_style_margin_all(obj, 0, 0); + lv_obj_set_style_outline_width(obj, 0, 0); + lv_obj_set_style_shadow_width(obj, 0, 0); + lv_obj_set_style_transform_width(obj, 0, 0); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_radius(obj, 0, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); +} + +static inline lv_obj_t* uiObjCreate(lv_obj_t* parent) { + lv_obj_t* obj = lv_obj_create(parent); + uiResetWidget(obj); + return obj; +} + +static inline lv_obj_t* uiButtonCreate(lv_obj_t* parent) { + lv_obj_t* btn = lv_button_create(parent); + uiResetWidget(btn); + lv_obj_set_style_bg_color(btn, uiBtnColor(), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(btn, 8, 0); + lv_obj_set_style_pad_hor(btn, 10, 0); + lv_obj_set_style_pad_ver(btn, 6, 0); + lv_obj_set_scrollable(btn, false); + return btn; +} + +static inline lv_obj_t* uiLabelCreate(lv_obj_t* parent) { + lv_obj_t* label = lv_label_create(parent); + lv_obj_set_style_margin_all(label, 0, 0); + lv_obj_set_style_bg_opa(label, LV_OPA_TRANSP, 0); + lv_obj_set_style_text_color(label, uiTextColor(), 0); + return label; +} + +// Applied after the helper definitions so the helpers themselves still call +// the real LVGL constructors. +#define lv_obj_create uiObjCreate +#define lv_button_create uiButtonCreate +#define lv_label_create uiLabelCreate diff --git a/Apps/M5UnitTest/main/Source/UiScale.h b/Apps/M5UnitTest/main/Source/UiScale.h index a35ae13..77caa37 100644 --- a/Apps/M5UnitTest/main/Source/UiScale.h +++ b/Apps/M5UnitTest/main/Source/UiScale.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include "UiKit.h" // Device screen widths in default (portrait) orientation: // tiny < 200 : small OLEDs, custom breadboard devices diff --git a/Apps/M5UnitTest/manifest.properties b/Apps/M5UnitTest/manifest.properties index bbe7d4b..709d399 100644 --- a/Apps/M5UnitTest/manifest.properties +++ b/Apps/M5UnitTest/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32s3,esp32p4 id=tactility.m5unittest -version.name=0.16.0 -version.code=16 +version.name=0.17.0 +version.code=17 name=M5 Unit Test app.0.id=tactility.m5unittest app.0.name=M5 Unit Test diff --git a/Apps/Magic8Ball/main/Source/Magic8Ball.cpp b/Apps/Magic8Ball/main/Source/Magic8Ball.cpp index 4d4115e..af3d57a 100644 --- a/Apps/Magic8Ball/main/Source/Magic8Ball.cpp +++ b/Apps/Magic8Ball/main/Source/Magic8Ball.cpp @@ -1,4 +1,5 @@ #include "Magic8Ball.h" +#include #include #include #include @@ -44,6 +45,29 @@ const char* getInputHint() { return "Touch the ball to ask"; } +void resizeBall(lv_obj_t* container, Context* ctx) { + const int32_t availableWidth = lv_obj_get_content_width(container); + const int32_t availableHeight = lv_obj_get_content_height(container); + const int32_t hintHeight = lv_obj_get_height(ctx->hintLabel); + const int32_t diameterByHeight = availableHeight - hintHeight - 12; + const int32_t diameter = availableWidth < diameterByHeight ? availableWidth : diameterByHeight; + if (diameter <= 0) return; + + lv_obj_set_size(ctx->ballObj, diameter, diameter); + lv_obj_set_style_border_width(ctx->ballObj, diameter / 48 > 2 ? diameter / 48 : 2, 0); + lv_obj_set_style_border_width(ctx->windowObj, diameter / 64 > 1 ? diameter / 64 : 1, 0); + + const auto fontSize = diameter >= 200 ? FONT_SIZE_LARGE + : diameter >= 128 ? FONT_SIZE_DEFAULT + : FONT_SIZE_SMALL; + lv_obj_set_style_text_font(ctx->answerLabel, lvgl_get_text_font(fontSize), 0); +} + +void onContainerSizeChanged(lv_event_t* e) { + auto* ctx = static_cast(lv_event_get_user_data(e)); + resizeBall(lv_event_get_target_obj(e), ctx); +} + /* ── Behavior ─────────────────────────────────────────────────────── */ void revealAnswer(Context* ctx) { @@ -95,7 +119,7 @@ void onKey(lv_event_t* e) { void magic8BallCreateWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); /* Toolbar */ @@ -109,44 +133,68 @@ void magic8BallCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_align_to(cont, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0); - lv_obj_set_style_pad_all(cont, 2, 0); - lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(cont, 12, 0); + lv_obj_set_style_pad_row(cont, 12, 0); + lv_obj_set_scrollable(cont, false); lv_obj_set_style_border_width(cont, 0, 0); - lv_obj_set_style_bg_color(cont, lv_color_hex(0x000000), 0); lv_obj_set_style_bg_opa(cont, LV_OPA_COVER, 0); lv_obj_set_style_radius(cont, 0, 0); - /* "8" ball circle */ + /* Black outer sphere */ ctx->ballObj = lv_obj_create(cont); lv_obj_set_size(ctx->ballObj, 120, 120); lv_obj_set_style_radius(ctx->ballObj, LV_RADIUS_CIRCLE, 0); - lv_obj_set_style_bg_color(ctx->ballObj, lv_color_make(0x10, 0x10, 0x30), 0); + lv_obj_set_style_bg_color(ctx->ballObj, lv_color_hex(0x090D16), 0); lv_obj_set_style_bg_opa(ctx->ballObj, LV_OPA_COVER, 0); - lv_obj_set_style_border_color(ctx->ballObj, lv_color_make(0x40, 0x40, 0x80), 0); + lv_obj_set_style_border_color(ctx->ballObj, lv_color_hex(0x394252), 0); lv_obj_set_style_border_width(ctx->ballObj, 3, 0); - lv_obj_remove_flag(ctx->ballObj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_shadow_color(ctx->ballObj, lv_color_hex(0x000000), 0); + lv_obj_set_style_shadow_width(ctx->ballObj, 8, 0); + lv_obj_set_style_pad_all(ctx->ballObj, 0, 0); + lv_obj_set_scrollable(ctx->ballObj, false); lv_obj_set_flex_flow(ctx->ballObj, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(ctx->ballObj, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - /* Answer text inside the ball */ - ctx->answerLabel = lv_label_create(ctx->ballObj); + /* Recessed blue answer window */ + ctx->windowObj = lv_obj_create(ctx->ballObj); + lv_obj_set_size(ctx->windowObj, LV_PCT(78), LV_PCT(78)); + lv_obj_set_style_radius(ctx->windowObj, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(ctx->windowObj, lv_color_hex(0x08234A), 0); + lv_obj_set_style_bg_opa(ctx->windowObj, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(ctx->windowObj, lv_color_hex(0x287AC0), 0); + lv_obj_set_style_border_width(ctx->windowObj, 2, 0); + lv_obj_set_style_pad_all(ctx->windowObj, 0, 0); + lv_obj_set_scrollable(ctx->windowObj, false); + lv_obj_set_flex_flow(ctx->windowObj, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(ctx->windowObj, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_gesture_bubble(ctx->windowObj, true); + + /* Answer text inside the window */ + ctx->answerLabel = lv_label_create(ctx->windowObj); lv_label_set_text(ctx->answerLabel, "8"); lv_obj_set_style_text_color(ctx->answerLabel, lv_color_hex(0xFFFFFF), 0); - lv_obj_set_style_text_font(ctx->answerLabel, lv_font_get_default(), 0); + lv_obj_set_style_text_font(ctx->answerLabel, lvgl_get_text_font(FONT_SIZE_LARGE), 0); lv_obj_set_style_text_align(ctx->answerLabel, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_set_width(ctx->answerLabel, 100); + lv_obj_set_width(ctx->answerLabel, LV_PCT(86)); lv_label_set_long_mode(ctx->answerLabel, LV_LABEL_LONG_WRAP); + lv_obj_set_gesture_bubble(ctx->answerLabel, true); /* Hint text below the ball */ ctx->hintLabel = lv_label_create(cont); lv_label_set_text(ctx->hintLabel, getInputHint()); lv_obj_set_style_text_color(ctx->hintLabel, lv_color_make(0x88, 0x88, 0x88), 0); lv_obj_set_style_text_font(ctx->hintLabel, lv_font_get_default(), 0); + lv_obj_set_style_text_align(ctx->hintLabel, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_width(ctx->hintLabel, LV_PCT(100)); /* Make the ball tappable / also space */ - lv_obj_add_flag(ctx->ballObj, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_clickable(ctx->ballObj, true); lv_obj_add_event_cb(ctx->ballObj, onBallClick, LV_EVENT_CLICKED, ctx); + lv_obj_update_layout(cont); + resizeBall(cont, ctx); + lv_obj_add_event_cb(cont, onContainerSizeChanged, LV_EVENT_SIZE_CHANGED, ctx); + /* Keyboard support - no editing mode needed, just focus the ball */ if (device_has_active_by_type(&KEYBOARD_TYPE)) { lv_group_t* grp = lv_group_get_default(); @@ -166,4 +214,5 @@ void magic8BallTeardown(Context* ctx) { ctx->answerLabel = nullptr; ctx->hintLabel = nullptr; ctx->ballObj = nullptr; + ctx->windowObj = nullptr; } diff --git a/Apps/Magic8Ball/main/Source/Magic8Ball.h b/Apps/Magic8Ball/main/Source/Magic8Ball.h index 7434ef9..1975ac9 100644 --- a/Apps/Magic8Ball/main/Source/Magic8Ball.h +++ b/Apps/Magic8Ball/main/Source/Magic8Ball.h @@ -12,6 +12,7 @@ struct Context { lv_obj_t* answerLabel = nullptr; lv_obj_t* hintLabel = nullptr; lv_obj_t* ballObj = nullptr; + lv_obj_t* windowObj = nullptr; // State int lastIdx = -1; diff --git a/Apps/Magic8Ball/manifest.properties b/Apps/Magic8Ball/manifest.properties index 3d66ec3..d555c33 100644 --- a/Apps/Magic8Ball/manifest.properties +++ b/Apps/Magic8Ball/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.magic8ball -version.name=0.17.0 -version.code=17 +version.name=0.18.0 +version.code=18 name=Magic 8-Ball app.0.id=tactility.magic8ball app.0.name=Magic 8-Ball diff --git a/Apps/MediaKeys/README.md b/Apps/MediaKeys/README.md new file mode 100644 index 0000000..a53bc31 --- /dev/null +++ b/Apps/MediaKeys/README.md @@ -0,0 +1,56 @@ +# Media Keys + +Turns the device into a Bluetooth LE media remote: six on-screen buttons for previous, play/pause, +next, mute, volume down and volume up, sent as HID Consumer Control reports. A host pairs it like any +other Bluetooth remote. + +## Requirements + +- **Bluetooth.** The Bluetooth device has to be enabled in the devicetree. The radio itself may be + off: switching the remote on turns it on, and the app turns it back off when it exits if it was + off before. If the radio is already on when the app opens, the remote switches on by itself. +- **A host.** Buttons are ignored while no host is connected. + +## Pairing + +The app advertises as `Tactility Media Keys` with its own Bluetooth identity (address and bond), so a +host pairs it as a separate device from the other Tactility Bluetooth apps (Air Mouse, MIDI). Pair it +once per host. Switching apps does not need a new pairing. + +## Main screen + +The toolbar has the title and a switch. + +- **Switch:** turns the remote on and off. On means: Bluetooth radio on, HID keyboard/consumer + profile started, advertising, and the buttons sending consumer reports. Switching it off stops the + profile and puts the radio back the way it was. Opening the app while the radio is already on + switches it on. +- **Buttons:** previous track, play/pause, next track, mute, volume down, volume up. They do nothing + when no host is connected. +- When the Bluetooth radio is turned off elsewhere (Tactility's own settings), the switch turns + itself off. + +## Physical keyboard + +When a keyboard device is active, the app also accepts keys while the remote is on: + +| Key | Action | +| --- | --- | +| `b` | Previous track | +| `p` | Play/pause | +| `n` | Next track | +| `m` | Mute | +| `d` | Volume down | +| `u` | Volume up | + +`Q` or `Esc` leaves keyboard mode so the keyboard can navigate the UI again. Switch the remote off +and on to re-enter it. + +## Limitations + +- Media keys only: no mouse, and the keyboard part of the HID profile is not used. +- The app cannot start while another Bluetooth device is connected to Tactility in host mode (for + example a paired BLE keyboard). Disconnect that device first. +- Closing the app stops the HID profile, so the host shows the device as disconnected until the app + is opened again. +- Devices without Bluetooth can open the app but cannot pair. diff --git a/Apps/MediaKeys/main/Source/MediaKeys.cpp b/Apps/MediaKeys/main/Source/MediaKeys.cpp index b210d68..a5ba640 100644 --- a/Apps/MediaKeys/main/Source/MediaKeys.cpp +++ b/Apps/MediaKeys/main/Source/MediaKeys.cpp @@ -62,7 +62,7 @@ struct SendKeyData { void enterKeyMode(Context* ctx); void exitKeyMode(Context* ctx); -void startHid(Context* ctx, bool renameAfterStart); +void startHid(Context* ctx); void postWorkerCommand(Context* ctx, WorkerCommandType type); void handleButtonPress(Context* ctx, uint32_t buttonId); @@ -198,7 +198,7 @@ void handleBtEvent(Context* ctx, const BtEvent& event) { if (window_manager_get_state(ctx->window) == WINDOW_STATE_GRANTED) { if (device_has_active_by_type(&KEYBOARD_TYPE)) exitKeyMode(ctx); if (ctx->switchWidget) lv_obj_remove_state(ctx->switchWidget, LV_STATE_CHECKED); - if (ctx->mainWrapper) lv_obj_add_flag(ctx->mainWrapper, LV_OBJ_FLAG_HIDDEN); + if (ctx->mainWrapper) lv_obj_set_hidden(ctx->mainWrapper, true); } if (ctx->hidDevice) device_put(ctx->hidDevice); ctx->hidDevice = nullptr; @@ -242,9 +242,18 @@ void showHidFailed(Context* ctx) { } // Runs on the worker, once the BT radio is confirmed ON (either already was, or just came up). -void startHid(Context* ctx, bool renameAfterStart) { +void startHid(Context* ctx) { ctx->radioEnabling = false; + if (!ctx->btDevice) { + LOG_E(TAG, "No Bluetooth device"); + showHidFailed(ctx); + return; + } + // Name before the profile start. The name is also the default BLE identity, and + // bluetooth_hid_device_start() applies it to an already-running radio. + bluetooth_set_device_name(ctx->btDevice, DEVICE_NAME); + Device* hidDevice = bluetooth_hid_device_get(); if (!hidDevice) { LOG_E(TAG, "BLE HID device unavailable after radio on"); @@ -261,19 +270,13 @@ void startHid(Context* ctx, bool renameAfterStart) { } ctx->hidDevice = hidDevice; - // Rename only after HID is up: with the radio on, a rename restarts advertising - // right away, and the GATT profile switch in bluetooth_hid_device_start() asserts in - // NimBLE (ble_svc_gap_init) if it runs while that advertising is still active. - // Once HID is active, the rename just restarts HID advertising with the new name. - if (renameAfterStart) bluetooth_set_device_name(ctx->btDevice, DEVICE_NAME); - // Skip touching widgets if the window was buried in the meantime (window_manager may have // already deleted them), same reasoning as onKeyHighlightTimer above. // Also skip if the switch was turned off while HID was starting: handleSwitchToggle already // hid the controls and queued Disable, which stops HID. if (!lockLvgl()) return; if (ctx->isEnabled && window_manager_get_state(ctx->window) == WINDOW_STATE_GRANTED) { - if (ctx->mainWrapper) lv_obj_remove_flag(ctx->mainWrapper, LV_OBJ_FLAG_HIDDEN); + if (ctx->mainWrapper) lv_obj_set_hidden(ctx->mainWrapper, false); if (device_has_active_by_type(&KEYBOARD_TYPE)) enterKeyMode(ctx); } lvgl_unlock(); @@ -300,21 +303,33 @@ void workerTask(void* param) { break; case WorkerCommandType::StartHid: // Re-check here to avoid a race with a switch-off that happened after the command was queued - if (ctx->isEnabled) startHid(ctx, true); + if (ctx->isEnabled) startHid(ctx); break; case WorkerCommandType::StartHidAfterRadioOn: // Re-check here to avoid a race with a switch-off that happened after the event was queued - if (ctx->isEnabled && ctx->radioEnabling) startHid(ctx, false); + if (ctx->isEnabled && ctx->radioEnabling) startHid(ctx); break; case WorkerCommandType::Disable: if (ctx->hidDevice) { - bluetooth_hid_device_stop(ctx->hidDevice); + if (ctx->btDevice && bluetooth_is_radio_on_or_pending(ctx->btDevice)) { + bluetooth_hid_device_stop(ctx->hidDevice); + } device_put(ctx->hidDevice); ctx->hidDevice = nullptr; } restoreRadioIfNeeded(ctx); break; case WorkerCommandType::Shutdown: + // Stop HID before restoring the radio, exactly like Disable. Leaving the + // profile active after the app exits keeps it advertising, so the paired PC + // reconnects to a device that has no app behind it. + if (ctx->hidDevice) { + if (ctx->btDevice && bluetooth_is_radio_on_or_pending(ctx->btDevice)) { + bluetooth_hid_device_stop(ctx->hidDevice); + } + device_put(ctx->hidDevice); + ctx->hidDevice = nullptr; + } restoreRadioIfNeeded(ctx); running = false; break; @@ -328,12 +343,9 @@ void teardownBt(Context* ctx) { // Unsubscribe FIRST - stops any in-flight BT events from firing against our (possibly // already freed) UI widget pointers after this returns. if (ctx->btDevice) bluetooth_event_unsubscribe(ctx->btDevice, &ctx->btEventSub); - // Do NOT call bluetooth_hid_device_stop here: it calls ble_gatts_reset() / - // ble_gatts_start() which corrupts NimBLE heap while the host task is still - // running. HID device is a persistent kernel device; hid_device_start() cleans - // up stale context on next use. Explicit stop is handled by handleSwitchToggle. - // The device itself is never stopped, it's started for the process lifetime (see - // Documentation/bluetooth-app-migration.md) - just restore the radio and drop our ref. + // HID is stopped by the worker's Shutdown command (it owns ctx->hidDevice and must + // stop the profile before restoring the radio). The HID *device* object itself is a + // persistent kernel device and is only unreferenced here. if (ctx->workerQueue != nullptr) { // Blocking send: dropping Shutdown on a full queue would hang the workerDone wait below. // The worker keeps draining the queue, so this completes. @@ -387,7 +399,7 @@ void handleSwitchToggle(Context* ctx, bool enabled) { // Explicit user toggle-off: the worker stops HID cleanly and restores the radio // (safe here since the user intentionally disabled, so no race with app teardown). postWorkerCommand(ctx, WorkerCommandType::Disable); - if (ctx->mainWrapper) lv_obj_add_flag(ctx->mainWrapper, LV_OBJ_FLAG_HIDDEN); + if (ctx->mainWrapper) lv_obj_set_hidden(ctx->mainWrapper, true); } } @@ -463,7 +475,7 @@ void mediaKeysProcessBtEvents(Context* ctx) { void mediaKeysCreateWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Media Keys"); @@ -478,7 +490,7 @@ void mediaKeysCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_grow(ctx->mainWrapper, 1); lv_obj_set_style_pad_all(ctx->mainWrapper, 4, LV_PART_MAIN); lv_obj_set_style_border_width(ctx->mainWrapper, 0, LV_PART_MAIN); - lv_obj_remove_flag(ctx->mainWrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->mainWrapper, false); lv_obj_set_flex_flow(ctx->mainWrapper, LV_FLEX_FLOW_COLUMN); ctx->buttonMatrix = lv_btnmatrix_create(ctx->mainWrapper); @@ -507,7 +519,7 @@ void mediaKeysCreateWidgets(lv_obj_t* parent, void* userData) { lv_timer_pause(ctx->keyHighlightTimer); } - lv_obj_add_flag(ctx->mainWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->mainWrapper, true); // Auto-enable if BT is already on (turned on via QuickPanel/Settings before opening app). // ctx->btDevice was already acquired by mediaKeysInitBt() before this ran. diff --git a/Apps/MediaKeys/manifest.properties b/Apps/MediaKeys/manifest.properties index 3030e08..556f669 100644 --- a/Apps/MediaKeys/manifest.properties +++ b/Apps/MediaKeys/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32s3,esp32p4 id=tactility.mediakeys -version.name=0.20.0 -version.code=20 +version.name=0.21.0 +version.code=21 name=Media Keys description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus. app.0.id=tactility.mediakeys diff --git a/Apps/MidiController/CMakeLists.txt b/Apps/MidiController/CMakeLists.txt new file mode 100644 index 0000000..f4c4dce --- /dev/null +++ b/Apps/MidiController/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(midicontroller) +project(midicontroller) +tactility_project_post(midicontroller) diff --git a/Apps/MidiController/README.md b/Apps/MidiController/README.md new file mode 100644 index 0000000..f5c4508 --- /dev/null +++ b/Apps/MidiController/README.md @@ -0,0 +1,52 @@ +# MidiController - Documentation + +MIDI controller app for the M5Stack Tab5 (ESP32-P4) built on the Tactility SDK. +Sends standard 3-byte MIDI over Bluetooth LE MIDI or USB MIDI to any DAW, synth, or hardware. + +## Quick start + +1. Pick **BLUETOOTH LE** or **USB** on the transport screen shown at launch (unless BLE MIDI is already running). You can change it later with the `USB`/`BLE` button in the toolbar while sending is stopped. +2. Flip the toolbar switch to start sending. For BLE, pair with your DAW/synth - the device advertises as **Tactility MIDI**. +3. Tap any mode tile on the home screen to begin. + +Hardware accessories (8Encoder, LCD Unit) are optional - the app works without them. + +## Bluetooth notes + +The device advertises as `Tactility MIDI` with its own Bluetooth address and bond, so a host sees it +as a separate device from the other Tactility Bluetooth apps (Air Mouse, Media Keys). Pair it once +per host. Switching apps does not need a new pairing, the host reconnects each device on sight. + +### Windows users + +Windows users will need a BLE Midi Bridge of some sort after pairing to the pc. +I can recommend either of these two, which have other needed information to set things up. +https://github.com/Maxime-J/BLE-MIDI-Bridge +https://github.com/mayerwin/Perfect-Bluetooth-MIDI-For-Windows + +### Linux users + +Linux - BlueZ may need recompilation depending on distro. Possibly helpful info https://linuxmusicians.com/viewtopic.php?t=24481 + +### MAC users +MAC - Bluetooth midi should be supported by default i believe. + +## USB Notes + +Nothing, USB Midi should just be plug n play. Select the `Tactility MIDI` device in your DAW/Synth etc + +## Contents + +| File | What it covers | +|------|---------------| +| [hardware.md](docs/hardware.md) | Tab5 device, Port A wiring, I2C units, PaHub channel map | +| [architecture.md](docs/architecture.md) | Code layout, mode lifecycle, LVGL patterns, BLE/USB MIDI plumbing | +| [modes.md](docs/modes.md) | All 15 modes - parameters, controls, musical function | +| [encoder.md](docs/encoder.md) | 8Encoder knob/button/LED assignments per mode | +| [lcd.md](docs/lcd.md) | LCD Unit display layouts per mode | +| [scales.md](docs/scales.md) | Scale and key system, MIDI note helpers | +| [units.md](docs/units.md) | Unit drivers in `Libraries/M5UnitModules` (8Encoder, PaHub, LCD, ByteButton, Joystick2, Scroll) | + +## Screenshots (more screenshots in the images folder) + +![alt text](images/0_transport_selection.png) ![alt text](images/1_mode_selection.png) diff --git a/Apps/MidiController/docs/MIDI-Control-Change-Messages_Another-Producer.pdf b/Apps/MidiController/docs/MIDI-Control-Change-Messages_Another-Producer.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d113dfce7fc35abccf86f5f94cb716e3c799a0af GIT binary patch literal 182253 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

$vs?R^n;q6p&zT1zzWd(QwfOZ-PJaI8 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

#x-VVTV=yLy1*!7VdO8mxu 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 Ab5q*KXfl@NRQWtCHRECi9R}X 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�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 - create new LVGL tree + 11. start the 20 ms UI timer when mode == BEATS + 12. lv_indev_wait_release() - discard any touch still in-flight + +teardown() (after the window is removed) + └─ encoderStop() - stop poll timer, LEDs off, lcdStop() + └─ stopAllModeTimers() - also deletes the BEATS UI timer + └─ stop/reset the Sequencer + └─ stop BLE/USB, unsubscribe BT events, release device references + └─ resetAllState() - reset all state structs and UI pointers +``` + +### Why `lv_indev_wait_release` is called twice + +A tap on a home tile fires `switchToMode`. The finger-up event from that same tap can land on a button in the freshly built view if both share the same screen coordinates. The first `wait_release` runs before view construction but LVGL may still queue the event. A second call after construction ensures it is discarded before any button in the new view can process it. + +--- + +## State management + +Each mode has its own state struct nested inside `MidiController` (e.g. `RagaState raga_`). Data fields (bpm, notes, parameters) **persist** across mode switches; LVGL widget pointers are **nulled** before the old tree is destroyed and **re-populated** when the new view is built. + +`resetAllState()` (called from `teardown()`) resets every per-mode state struct and its UI pointers to defaults. + +--- + +## MIDI transport + +MIDI is sent over either Bluetooth LE or USB. On launch the app shows a transport chooser (unless BLE MIDI is already running). The toolbar `USB`/`BLE` button reopens the chooser while sending is stopped. The choice is stored in `activeTransport_` for the session. + +The toolbar switch calls `handleTransportToggle(true/false)`, which starts or stops the selected transport. + +**BLE:** uses Tactility's `bluetooth_midi` driver. +1. If the BT radio is off, `bluetooth_set_radio_enabled(true)` is called and `startBleMidi()` runs from the `BT_RADIO_STATE_ON` event callback. +2. `bluetooth_midi_start()` enables the MIDI profile and the app advertises as **Tactility MIDI**. The name is set before the profile starts, so the radio switches to the MIDI identity (own address and bond) when it is already on under another app's identity. +3. `sendMidi(cmd, d1, d2)` calls `bluetooth_midi_send()`. + +**USB:** `startUsbMidi()` calls `usb_midi_device_start()` and `sendMidi()` calls `usb_midi_device_send()`. + +On stop or hide, `CC 123` (All Notes Off) is sent before the transport is stopped. Every `*_get()` helper returns a referenced `Device`, so each caller pairs it with `device_put()`. + +--- + +## LVGL patterns used + +### Canvas-style drawing +ZEN, DROP, LFO, XY, TB3PO, and EUCLID draw into a plain container via +`LV_EVENT_DRAW_MAIN`. No `lv_canvas` buffer is allocated - the container is +freed with the view tree. + +TB3PO and EUCLID are sequencers, so their canvas is invalidated on every step. +They deliberately **draw the grid/rings directly** instead of deleting and +recreating child widgets: the earlier implementation churned through hundreds +of LVGL objects per beat, which could starve the idle task and trip the task +watchdog on slower targets. State changes only call `lv_obj_invalidate()`. + +ZEN and DROP move a handful of ball widgets. Moving a child invalidates its old +and new areas, and LVGL repaints the canvas background there, so those timers +avoid full-canvas invalidation unless the static scene changes (a wall flash, +platform edit, or label/note change). + +### Touch handling +KEYS uses `LV_EVENT_PRESSING` on the piano row plus per-key `PRESSED`/`RELEASED` handlers so a finger can slide across keys. XY uses `LV_EVENT_PRESSING` on the pad and derives CC values from finger X/Y. GRID uses per-cell `PRESSED`/`RELEASED` handlers with one held note per pointer slot. + +### Encoder poll timer +`EncoderInput.cpp` runs a single 100 ms LVGL timer. On each tick: +1. Check `btnSeedNext` - if set, read buttons silently to seed `btnPrev` and return. +2. Route to the per-mode dispatch function based on `currentMode_`. +3. Dispatch reads deltas + buttons, updates state, refreshes LVGL labels, flushes LEDs, calls `lcdUpdate`. + +### LED dirty-caching +`Unit8Encoder::flushLeds` maintains a 9-entry colour cache (8 encoder LEDs + 1 switch LED). It sends the 24-byte encoder-LED burst only if one of those eight changed, and writes the switch LED separately only if that entry changed. + +--- + +## MIDI messages sent + +| Command | Hex | Used for | +|---------|-----|----------| +| Note On | `0x90` | All melodic/drum note triggers | +| Note Off | `0x80` | All note releases | +| Control Change | `0xB0` | XY pad (CC 74/71 default), LFO modulation, All Notes Off (CC 123) | +| Pitch Bend | `0xE0` | LFO in pitch-bend mode | + +All messages are sent on channel 1 (command bytes `0x90`/`0x80`/`0xB0`/`0xE0` without channel offset). diff --git a/Apps/MidiController/docs/encoder.md b/Apps/MidiController/docs/encoder.md new file mode 100644 index 0000000..596f09f --- /dev/null +++ b/Apps/MidiController/docs/encoder.md @@ -0,0 +1,158 @@ +# 8Encoder + +The M5Stack 8Encoder unit connects through a **PaHub** and can use any of its six ports. The app scans all six software channels and accepts the encoder wherever it is found; A channel is accepted only after the unit's firmware-version register (`0xFE`) and address register (`0xFF`) validate, so a false ACK on an empty PaHub port cannot latch the scan onto the wrong channel. It provides 8 rotary encoders with push-buttons, individually addressable RGB LEDs, and one toggle switch with its own LED. + +The encoder is completely optional. If the PaHub or the encoder unit is not detected, the app operates in touch-only mode with no degradation. + +--- + +## Per-mode mappings + +Only five modes use the encoder. In all other modes the encoder is idle (LEDs off). + +### RAGA + +| Control | Action | +|---------|--------| +| Knob 0 | Raga selection (0–7, wraps) | +| Knob 1 | Tala selection (0–2, wraps) | +| Knob 2 | Root note (MIDI 24–96) | +| Knob 3 | BPM (40–240) | +| Knobs 4–7 | Unused | +| Button 0 | Play / Stop toggle | +| Button 1 | Drone On / Off toggle | +| Buttons 2–7 | Unused | + +**LED colours:** + +| Knob | Colour | Meaning | +|------|--------|---------| +| 0 | Orange `0xFF8800` | Raga | +| 1 | Deep orange `0xFF6600` | Tala | +| 2 | Cyan `0x00FFCC` | Root | +| 3 | Yellow `0xFFE000` | BPM | +| 4–7 | Off | - | + +--- + +### DIMENSIONS + +| Control | Action | +|---------|--------| +| Knob 0 | Parameter A (0–100) | +| Knob 1 | Parameter B (0–100) | +| Knob 2 | Parameter C (0–100) | +| Knob 3 | Parameter D (0–100) | +| Knob 4 | Equation (1–20) | +| Knob 5 | BPM (40–280) | +| Knob 6 | Octave offset (−4 to +4) | +| Knob 7 | Random amount (0–64) | +| Button 0 | Play / Stop toggle | +| Button 1 | Reset (A/B/C/D → 50, t → 0) | +| Buttons 2–7 | Unused | + +**LED colours** (match the UI label colours): + +| Knob | Colour | Parameter | +|------|--------|-----------| +| 0 | `0x88DDFF` (light blue) | A | +| 1 | `0x88FFCC` (mint) | B | +| 2 | `0xFFCC88` (peach) | C | +| 3 | `0xFF88CC` (pink) | D | +| 4 | `0xFF8844` (orange) | Equation | +| 5 | `0xFFE000` (yellow) | BPM | +| 6 | `0xAAFF88` (lime) | Octave | +| 7 | `0xFFAA44` (amber) | Random | + +--- + +### TB3PO + +| Control | Action | +|---------|--------| +| Knob 0 | Density (0–16) | +| Knob 1 | BPM (40–280) | +| Knob 2 | Key root (0–11, wraps) | +| Knob 3 | Octave (1–6) | +| Knob 4 | Scale selection (6 options, wraps) | +| Knob 5 | Regen (any delta draws a new random seed) | +| Knobs 6–7 | Unused | +| Button 0 | Play / Stop toggle | +| Button 1 | Regen (new random seed and pattern) | +| Buttons 2–7 | Unused | + +**LED colours:** While playing, animate with the current 32-step playback position. The steps are grouped into eight 4-step ranges: knob 0 = steps 1–4, knob 1 = steps 5–8, … knob 7 = steps 29–32. The knob for the active range is lit orange `0xFF6600`; the other seven are dark `0x221100`. All eight are dark while stopped. + +--- + +### BABY8 + +| Control | Action | +|---------|--------| +| Knob 0 | Step 1 note (0–127) | +| Knob 1 | Step 2 note | +| Knob 2 | Step 3 note | +| Knob 3 | Step 4 note | +| Knob 4 | Step 5 note | +| Knob 5 | Step 6 note | +| Knob 6 | Step 7 note | +| Knob 7 | Step 8 note | +| Button 0 | Toggle gate for step 1 | +| Button 1 | Toggle gate for step 2 | +| … | … | +| Button 7 | Toggle gate for step 8 | + +**LED colours:** +- Current playback step (while playing): white `0xFFFFFF` +- Gate on (not current): dark green `0x004400` +- Gate off: dark red `0x440000` + +--- + +### EUCLID + +| Control | Action | +|---------|--------| +| Knob 0 | Selected voice - Steps (1–32) | +| Knob 1 | Selected voice - Events (0–steps) | +| Knob 2 | Selected voice - Rotation (0–steps-1, wraps) | +| Knob 3 | Selected voice - Note (0–127) | +| Knob 4 | BPM (40–280) | +| Knob 5 | Voice 6 - Steps | +| Knob 6 | Voice 7 - Steps | +| Knob 7 | Voice 8 - Steps | +| Button 0 | Select voice 1 | +| Button 1 | Select voice 2 | +| … | … | +| Button 7 | Select voice 8 | + +**LED colours** (per-voice accent colours): + +| Knob/Button | Colour | Voice | +|-------------|--------|-------| +| 0 | `0xFF4040` Red | Voice 1 | +| 1 | `0xFF9900` Orange | Voice 2 | +| 2 | `0x44DD44` Green | Voice 3 | +| 3 | `0x44BBFF` Blue | Voice 4 | +| 4 | `0xFF44FF` Magenta | Voice 5 | +| 5 | `0xFFDD00` Yellow | Voice 6 | +| 6 | `0x00FFCC` Cyan | Voice 7 | +| 7 | `0xFF8888` Pink | Voice 8 | + +Selected voice LED is at full brightness; all others are dimmed to ¼ intensity (`>> 2` each channel). + +--- + +## Button edge detection and mode-switch safety + +Buttons use a rising-edge detector: `if (btn && !btnPrev[i])`. This means holding a button across a mode switch will not register as a press in the new mode. + +On each mode switch `btnSeedNext` is set. The very next encoder poll reads the live button state silently into `btnPrev` without firing any actions, then returns. This seeds the edge detector correctly regardless of which buttons are physically held. + +--- + +## Poll rate + +The encoder poll timer fires every **100 ms** (10 Hz). This is a balance between responsiveness and I2C bus traffic: each poll requires up to 16 individual I2C reads (8 deltas + 8 buttons) across the PaHub multiplexer. + +Delta values accumulate in the encoder hardware between polls, so fast knob turns are not lost - the delta register counts all movement since the last read. diff --git a/Apps/MidiController/docs/hardware.md b/Apps/MidiController/docs/hardware.md new file mode 100644 index 0000000..afa321e --- /dev/null +++ b/Apps/MidiController/docs/hardware.md @@ -0,0 +1,133 @@ +# Hardware + +## Target device - M5Stack Tab5 + +| Property | Value | +|----------|-------| +| SoC | ESP32-P4 | +| Display | 1280×720 MIPI DSI (built-in) | +| OS | Tactility RTOS / FreeRTOS | +| Port A | Grove port in I2C mode - SDA=53, SCL=54 | + +All external hardware hangs off **Port A** via the PaHub multiplexer. + +--- + +## Wiring + +``` +Tab5 Port A + └─ PaHub (PCA9548AP, 0x70) + ├─ CH0 ── 8Encoder Unit (0x41) + └─ CH5 ── LCD Unit (0x3E) +``` + +The app scans all six software channels in order and uses the first matching unit it finds, so the port is not a hard requirement. Software channels are zero-based; the silkscreen on some PaHub units numbers the physical ports 1–6. + +Both units are **optional**. If the PaHub is not detected the app operates in touch-only mode. +If the PaHub is present but a unit is missing, that unit is silently disabled. + +--- + +## PaHub channel map + +| Channel | Unit | I2C address | +|---------|------|-------------| +| CH0 | 8Encoder | 0x41 | +| CH5 | LCD Unit (135×240) | 0x3E | +| Other | unused / available | - | + +The app scans CH0–CH5 at startup, so either unit can be moved to another free channel. + +--- + +## I2C unit summary + +### 8Encoder (0x41) +8 rotary encoders with push-buttons, individually addressable RGB LEDs (3-byte RGB), and one toggle switch with its own LED. + +Key registers: + +| Register | Description | +|----------|-------------| +| `0x20 + i*4` | Counter delta since last read (int32_t, 4 pulses/detent) | +| `0x50 + i` | Button state (0 = released, non-zero = pressed) | +| `0x60` | Toggle-switch state (0 = off, 1 = on) | +| `0x70 + i*3` | Encoder LED RGB (3-byte stride, `i` = 0–7) | +| `0x88` | Toggle-switch LED RGB (3 bytes) | + +### PaHub v2.1 (0x70 - PCA9548AP) +PCA9548AP-based I2C multiplexer with **6 exposed channels (CH0–CH5)**; the chip itself supports 8, but only six ports are wired on the unit. Write a single bitmask byte to select a channel; write `0x00` to deselect all. + +``` +select(ch) → write(1 << ch) → read back for diagnostics (not fatal) +deselect() → write(0x00) → read back → require 0x00 (3 attempts) +``` + +The control register is readable on the upstream bus. `select()` logs a +readback mismatch but still proceeds, because a downstream STM32 can hold SCL +low and block the readback even when the mux latched correctly. Device identity +checks then reject a wrong or empty channel. `deselect()` is strict: with no +channel selected the readback must be `0x00`, which also prevents a non-mux +device at `0x70` from being accepted as a PaHub at startup. + +### LCD Unit (0x3E - ESP32-PICO inside) +135×240 IPS display (portrait default). The internal ESP32 bridges I2C commands directly to an ST7789V2. +Unlike the STM32-based units it uses a **command-buffer protocol** - no STOP+delay pattern needed. + +Key commands: + +| Command | Description | +|---------|-------------| +| `0x04` | Read 4-byte ID: `0x77 0x89 [version hi] [version lo]` | +| `0x22 [brightness]` | Set backlight (0=off, 255=max) | +| `0x36 [rotation]` | Set rotation (0–3 = 0°/90°/180°/270°) | +| `0x6A [x0][y0][x1][y1][hi][lo]` | Fill rectangle with inline RGB565 colour | +| `0x62 [x][y][hi][lo]` | Draw single pixel RGB565 | +| `0x2A [x0][x1]` / `0x2B [y0][y1]` | Set active window (column / row range) | +| `0x42 [pixels…]` | Write raw RGB565 stream into active window | +| `0x09` | Read buffer-remaining count (1-byte response) | + +Used in portrait orientation (rotation 0): **135 px wide × 240 px tall**. + +### ByteButton (0x47) - available, not wired +8 push-buttons with 4-byte LE RGB888 LEDs per button. + +| Register | Description | +|----------|-------------| +| `0x00` | Button bitmask (bit i = button i) | +| `0x60 + i` | Individual button state | +| `0x20 + i*4` | LED colour [B, G, R, 0x00] | +| `0x19 = 0x00` | Show-mode register (must be written on init) | + +### Joystick2 (0x63) - available, not wired +Dual-axis thumbstick with button and RGB LED. + +| Register | Description | +|----------|-------------| +| `0x50` | XY 12-bit offset-corrected (2×int16) | +| `0x20` | Button (0 = pressed) | +| `0x30` | LED LE uint32 RGB | + +### Scroll (0x40) - available, not wired +Single rotary encoder with button and RGB LED. + +| Register | Description | +|----------|-------------| +| `0x50` | Delta since last read (int16_t, resets on read) | +| `0x20` | Button (0 = pressed) | +| `0x30` | LED [0x00, R, G, B] | +| `0x40` | Reset encoder counter (write 1) | + +--- + +## I2C protocol notes + +**STM32-based units** (ByteButton, Joystick2, Scroll, 8Encoder): +- Use a **write-then-read** pattern with a mandatory 3 ms delay between the address write (STOP) and the data read. +- A repeated-START without the STOP causes the STM32 HAL to return stale data. +- Helper: `unitReadReg()` in `UnitCommon.h` handles this automatically. + +**ESP32-based units** (LCD Unit): +- Standard I2C - no delay needed. Each write is a self-contained command. +- Pixel streaming ends at the I2C STOP condition. diff --git a/Apps/MidiController/docs/lcd.md b/Apps/MidiController/docs/lcd.md new file mode 100644 index 0000000..d762a9b --- /dev/null +++ b/Apps/MidiController/docs/lcd.md @@ -0,0 +1,162 @@ +# LCD Unit Display + +The M5Stack LCD Unit (135×240 IPS, portrait) connects through a **PaHub** and can use any of its six ports. The app scans all six software channels and accepts the display wherever it is found; A channel is accepted only after the unit returns its four-byte ID (`0x77 0x89 ...`), so a false ACK on an empty PaHub port cannot latch the scan onto the wrong channel. It shows a live secondary display that mirrors and extends the current mode's state. + +The LCD is completely optional. If the PaHub or the LCD unit is not detected, `lcdUpdate` is a no-op throughout. + +--- + +## General layout + +The display is **135 px wide × 240 px tall** in portrait orientation (rotation 0). + +Every layout starts with a **16 px coloured header bar** (plus a 1 px separator) at the top. For the five encoder modes it shows the mode name in 2× scale text, using that mode's accent colour; other modes show a generic `MIDI` header. + +A **5×7 bitmap font** (printable ASCII 32–126) is used for all text rendering. Two scale factors are used: +- **1× scale** - 5×7 px glyphs (very compact, for labels and values) +- **2× scale** - 10×14 px glyphs (for names and primary values) + +Colours are RGB565. The background is always black (`0x0000`). + +--- + +## Per-mode displays + +### HOME / Other non-encoder modes + +``` +┌─────────────────┐ +│ MIDI │ ← 2× text on grey background (16 px) +│ │ +│ │ +│ (blank) │ +└─────────────────┘ +``` + +Shows the generic `MIDI` header. The content area is blank (these modes don't use the encoder or LCD detail view). + +--- + +### RAGA + +``` +┌─────────────────┐ y=0 +│ RAGA │ header (16 px, orange) +│ │ +│ Bhairavi │ ← raga name, 2× orange (y=22) +│ │ +│ TALA Teental │ y=68 +│ ROOT C3 120 │ y=82 (root + BPM on same row) +├─────────────────┤ y=98 +│ PLAYING │ ← red/dark 28 px bar (y=100) +│ DRONE ON │ ← blue/dark 20 px bar (y=132) +│ │ +│ K1 RAGA │ ← knob legend (1× text, y=170) +│ K2 TALA │ +│ K3 ROOT │ +│ K4 BPM │ +│ B1 PLAY │ +│ B2 DRON │ +└─────────────────┘ +``` + +**Dirty fields:** Raga name, tala, root note, BPM, play state, drone state - each redraws only when its value changes. + +--- + +### DIMENSIONS + +``` +┌─────────────────┐ +│ DIM │ header (cyan) +│ A 50 B 50 │ ← labels + values, y=20 +│ [████] [████] │ ← filled colour bars, y=30–38 +│ C 50 D 50 │ ← y=50 +│ [████] [████] │ ← bars, y=60–68 +│ EQ 3 BPM 120│ ← y=82 +│ OCT +0 RND 0 │ ← y=96 +├─────────────────┤ y=110 +│ STOPPED │ ← green=playing, dark=stopped (y=112) +└─────────────────┘ +``` + +A/B share the top row (x=2 and x=67); C/D share the next row. Each has a colour bar showing the 0–100 value as a filled rectangle. + +**Bar colours** match the encoder LED and UI label colours: A=`#88DDFF`, B=`#88FFCC`, C=`#FFCC88`, D=`#FF88CC`. + +--- + +### TB3PO + +``` +┌─────────────────┐ +│ TB-3PO │ header (green) +│ ┌──┬──┬──┬──┐ │ ← 32-step piano roll (2 rows of 16) +│ │ │██│ │ │… │ each step: 6×12 px cell +│ └──┴──┴──┴──┘ │ white=current (while playing), green=gate on, dark=no gate +│ │ yellow accent dot = accent step +│ KEY C SCALE Major│ +│ DEN 8 BPM 120 │ +├─────────────────┤ +│ STOPPED │ +└─────────────────┘ +``` + +The piano roll starts at y=20, with two rows on a 15 px pitch (cells are 6×12 px). Row 0 = steps 0–15, row 1 = steps 16–31. Each cell has a 2 px gap between neighbours. Accent steps show a small yellow dot at the top-left corner. + +--- + +### BABY8 + +``` +┌─────────────────┐ +│ BABY8 │ header (magenta) +├─────────────────┤ +│ 1 C4 [■] │ ← step row 27 px tall; note name 2×, gate dot +│ 2 D4 [■] │ white bg = current step (gate on) +│ 3 E4 [ ] │ white bg + dark dot = current step (gate off) +│ 4 F4 [■] │ +│ 5 G4 [■] │ +│ 6 A4 [■] │ +│ 7 B4 [■] │ +│ 8 C5 [■] │ +└─────────────────┘ +``` + +Each step gets 25 px of height (27 px total with a 2 px separator line). The step number is on the left, note name (2×) in the centre-left, and a filled gate dot on the right. While playing, the current step has a white background with black text; when its gate is off the gate dot is dark instead of green. No step is highlighted while stopped. + +--- + +### EUCLID + +``` +┌─────────────────┐ +│ EUCLID │ header (red) +│ [1][2][3][4][5][6][7][8] ← voice tabs y=18–33, 15×15 px +│ VOICE 3 │ ← selected voice name, 2× in voice colour (y=38) +│ │ +│ STEPS 8 │ ← y=80 +│ EVENTS 5 │ ← y=94 +│ ROT 2 │ ← y=108 +│ NOTE C2 │ ← y=122 +│ BPM 120 │ ← y=140 +├─────────────────┤ y=154 +│ STOPPED │ ← y=156–174 +│ K1 STEPS │ ← knob legend y=176, 10 px rows +│ K2 EVENTS │ +│ K3 ROT │ +│ K4 NOTE │ +│ K5 BPM │ +│ B VOICE │ +└─────────────────┘ +``` + +Voice tabs start at y=18 with a 17 px pitch. The selected voice tab is drawn at full colour; others are dark grey. When the voice changes, the name, all parameter values, and the knob legend are all redrawn. + +--- + +## Dirty-caching + +`lcdUpdate` is called from the 100 ms encoder poll for the five encoder modes, and immediately on mode switch. Each mode maintains a cache of the last-drawn values. Only fields that have changed are redrawn, keeping bus traffic low. TB3PO and BABY8 also distinguish a pattern change (all step cells repainted) from a normal cursor advance (only the old and new cursor cells repainted). + +On a mode switch `lcdSetMode` clears the per-mode cache structs, seeds their change sentinels, and sets `s_dirty = true`, forcing a full screen redraw on the next `lcdUpdate` call. diff --git a/Apps/MidiController/docs/modes.md b/Apps/MidiController/docs/modes.md new file mode 100644 index 0000000..81794dd --- /dev/null +++ b/Apps/MidiController/docs/modes.md @@ -0,0 +1,300 @@ +# Modes + +The home screen shows a 3×5 grid of tiles. Tapping any tile switches to that mode. +Tap the **←** back button (top-left of most views) to return home. + +--- + +## KEYS - Chromatic Piano + +Single octave of piano keys. Drag your finger across keys for continuous glide. Multiple fingers play independently; each finger tracks its own note. + +**Parameters:** +- Octave (1–8) - base octave of the keyboard +- Scale / key root - shown in the status and control rows only; the keyboard stays fully chromatic and is not transposed + +**Controls:** OCT− / OCT+ buttons; SCALE cycle; KEY− / KEY+ + +**MIDI:** Note On on finger contact, Note Off on release. Each touch slot holds and slides its own note; lifting one finger does not release the others. + +**Scale/key controls** use the same scale table as RNG, ZEN, and DROP, but KEYS keeps its own selection and does not apply it to the keyboard. CHORD uses the same scale table with its key fixed at C - see [scales.md](scales.md). + +--- + +## BEATS - 4-Track Drum Sequencer + +Classic 4×16 step sequencer. Steps loop continuously while playing. + +**Tracks:** +| Track | Default MIDI note | Colour | +|-------|------------------|--------| +| KICK | 36 (C2) | Red | +| SNARE | 38 (D2) | Yellow | +| HIHAT | 42 (F#2) | Cyan | +| OPEN | 46 (A#2) | Purple | + +**Controls:** Tap any step cell to toggle it. PLAY/STOP, CLEAR (stops playback and turns all steps off), BPM− / BPM+. + +**Note duration:** Each track has a fixed note-off delay (KICK 200 ms, SNARE 150 ms, HIHAT 50 ms, OPEN 300 ms). + +--- + +## ZEN - Ambient Physics + +Up to 4 balls bounce around a rectangular arena. Each wall segment triggers a scale note on collision. + +**Wall layout:** 8 top segments, 8 bottom segments, 4 left segments, 4 right segments (24 total). Each segment maps to a scale degree; walls flash white briefly on hit. + +**Controls:** ADD (spawn ball), RESET (remove all but one), SCALE cycle, KEY± , OCT. + +**Parameters:** Scale, key, octave (2–6), ball count (1–4). + +**MIDI:** Random velocity (70–109). Note Off follows 150 ms after each Note On; walls flash for 200 ms on impact. + +--- + +## DROP - Gravity Platform Sequencer + +Balls roll and fall under gravity, triggering notes when they land on platforms. + +Two modes - **EDIT** (tap to place horizontal platforms) and **DROP** (tap to spawn balls). + +**Controls:** Toggle EDIT/DROP, CLEAR (removes balls and platforms), scale/key/octave. + +**Parameters:** Scale, key, octave (2–7), up to 6 platform positions. Platforms are always horizontal and receive a note automatically from the selected scale by platform index (degrees 0, 2, 4, 6, 1, 3). + +--- + +## RNG - Random Melody Generator + +Generates random melodies constrained to a scale, key, octave range, and probability. + +**Parameters:** +| Parameter | Range | Default | +|-----------|-------|---------| +| BPM | 40–300 | 120 | +| Subdivision | ¼ / ⅛ / 1/16 | ¼ | +| Min octave | 1–8 (≤ max) | 3 | +| Max octave | 1–8 (≥ min) | 6 | +| Probability | 0–100 % | 50 % | +| Scale | 6 options | Major | +| Key | C–B | C | + +**Controls:** PLAY/STOP, BPM± (step 5), KEY±, SCALE cycle, PROB± (step 5), MinOct±, MaxOct±, Subdivision cycle. + +On each tick, a note is chosen randomly from `(scale, key, octave range)` with the given probability. It plays for 75 % of the step duration, then Note Off. + +--- + +## XY - 2D CC Pad + +Full-screen touch pad. Finger X maps to one CC, finger Y maps to another. + +**Parameters:** +| Axis | Default CC | Suggested use | +|------|-----------|---------------| +| X | 74 | Filter cutoff | +| Y | 71 | Resonance | + +**Controls:** X CC± / Y CC± buttons to change target CC numbers (0–127). + +MIDI CC is sent continuously while the finger is held. Values range 0–127 across the full pad width/height. + +--- + +## ARP - Chord Arpeggiator + +Touch a note on the mini piano to latch an arpeggio of the selected chord type at the selected pattern and speed. + +**Chord types:** Major, Minor, Dominant 7th. + +**Patterns:** UP, DOWN, UP-DOWN, RANDOM, CHANCE (probabilistic skips). + +**Speed:** Quarter / Eighth / Sixteenth / 32nd note. + +**Parameters:** BPM, arpeggio pattern, chord type, octave range (1–4 octaves), piano octave (1–7). + +**Controls:** Tap piano key (bottom row) to start arp. Pattern/Chord/OctRange/Speed/BPM controls at top. + +--- + +## GRID - Isomorphic Grid + +8 columns × 5 rows. Each row is offset by a perfect 4th (5 semitones) from the row below - the same layout as a Linnstrument. Every cell adds one semitone across the row. + +**Note layout:** +``` +Row 4 (top): root + 20 … 27 semitones +Row 3: root + 15 … 22 +Row 2: root + 10 … 17 +Row 1: root + 5 … 12 +Row 0 (bot): root + 0 … 7 (chromatic: C, C#, D, D#, E, F, F#, G at octave 3) +``` + +**Cell colours:** C notes bright cyan; black-key notes dark blue; white-key notes medium blue; pressed cells bright cyan. + +**Controls:** OCT− / OCT+ (octave 1–6). + +Cells show the note name. Touch for Note On, release for Note Off. Multi-touch is supported: each pointer slot tracks its own held cell, so lifting one finger does not release notes held by the others. + +--- + +## CHORD - Diatonic Chord Player + +8 large buttons play chords built on the selected scale, with the root fixed at C. Chord VIII is chord I one octave up. + +**Chord voicings:** MAJ (root+M3+P5), MIN (root+m3+P5), DIM (root+m3+dim5). The chord qualities follow the major-scale degree pattern: I/IV/V major, ii/iii/vi minor, vii diminished, I+ major. The selected scale supplies the chord roots. + +**Controls:** Press a chord button to play; release to stop. OCT± / SCALE cycle / CLEAR. + +**Parameters:** Octave (1–7), scale. The key is fixed at C. + +--- + +## LFO - Low-Frequency Oscillator + +Continuously modulates a MIDI CC (or pitch bend) with a selectable waveform. + +**Waveforms:** Sine, Triangle, Square, Sawtooth. + +**Parameters:** +| Parameter | Range | Default | +|-----------|-------|---------| +| Rate | 0.1–10 Hz | 1 Hz | +| Amount | 0–127 | 64 | +| CC target | 0–127 | 1 (modwheel) | +| Mode | CC / Pitch | CC | + +**Controls:** START/STOP, Rate±, WAVE cycle, Amount±, CC±, PITCH mode toggle. + +In **Pitch** mode, a 14-bit pitch-bend message centred at 8192 is used instead of a CC. The actual semitone range depends on the receiver's pitch-bend setting (commonly ±2 semitones). The waveform is drawn live on the canvas. + +--- + +## RAGA - Indian Classical Generator + +Generates melodic phrases based on 8 classical Indian ragas with tala (rhythmic cycle) structure and an optional drone note. + +**Ragas:** +| # | Name | Character | +|---|------|-----------| +| 0 | Bhairavi | Morning, devotional | +| 1 | Lalit | Pre-dawn | +| 2 | Bhupali | Evening, peaceful | +| 3 | Todi | Morning, intense | +| 4 | Madhuvanti | Afternoon | +| 5 | Meghmalhar | Monsoon | +| 6 | Yaman | Evening, romantic | +| 7 | Malkauns | Night, serious | + +**Talas:** +| # | Name | Beats | +|---|------|-------| +| 0 | Teental | 16 | +| 1 | Rupak | 7 | +| 2 | Jhaptal | 10 | + +**Parameters:** Raga (0–7), tala (0–2), root note (MIDI 24–96), BPM (40–240). + +**Controls:** RAGA±, TALA±, ROOT±, BPM±, PLAY/STOP, DRONE toggle. + +**How it works:** Bhairavi phrases use a 2nd-order Markov chain; the other seven ragas use a degree random walk. A new pitch is emitted every 16th note (BPM/4), while the selected tala supplies the beat-accent cycle. The drone note (root) sustains independently of the melody. + +**Encoder:** Knob 0=raga, 1=tala, 2=root, 3=BPM; Button 0=play/stop, Button 1=drone. + +--- + +## DIMENSIONS - Parametric Equation Sequencer + +20 parametric equations map 4 parameters (A, B, C, D) and a running time variable (t) to a 3D position (px, py, pz). The position is quantised to a MIDI note each tick. + +**Parameters:** +| Param | Range | Description | +|-------|-------|-------------| +| Equation | 1–20 | Selects the formula | +| A, B, C, D | 0–100 | Equation coefficients | +| Octave | −4 to +4 | Note range offset | +| Random | 0–64 | Adds stochastic perturbation | +| BPM | 40–280 | Tick rate | + +**Controls:** Equation±, PLAY/STOP, RESET (resets A/B/C/D to 50 and t to 0), BPM±, OCT±, Rnd±. + +**Equation variety:** Includes Lissajous figures, chaotic attractors, Fourier synthesis, and other 3D parametric forms. Each produces a different melodic contour as t advances. + +**Encoder:** Knobs 0–3 = A/B/C/D; Knob 4 = equation; Knob 5 = BPM; Knob 6 = octave; Knob 7 = random; Button 0 = play/stop; Button 1 = reset. + +--- + +## BABY8 - 8-Step Melodic Sequencer + +Classic Baby-8 / analogue-sequencer style. 8 steps, each with a note, velocity, and gate. Up to 8 patterns. + +**Step display:** +| State | Colour | +|-------|--------| +| Current playing step, gate on | White | +| Current playing step, gate off | Blue | +| Selected (not playing), gate on | Orange | +| Selected (not playing), gate off | Brown | +| Other step, gate on | Dark green | +| Other step, gate off | Dark grey | + +**Parameters per step:** Note (0–127), velocity (0–127), gate (on/off). + +**Global parameters:** BPM (40–280), swing (50–80 %), active pattern (1–8, stored as index 0–7). + +**Controls:** PLAY/STOP, BPM±, SWING±, tap step to select, NOTE±, VEL±, GATE toggle, PATTERN±. + +**Swing:** Lengthens even-numbered steps and shortens odd-numbered steps by the same amount, so the pair still fills two 16th notes and the tempo stays constant. At 50 % there is no swing; at 80 % the groove is heavy. + +**Encoder:** Knobs 0–7 = step note (by index); Buttons 0–7 = toggle step gate. + +--- + +## TB3PO - 303-Style Acid Bass + +Procedurally generated 32-step bass patterns in the spirit of the Roland TB-303. + +**Step properties:** Note degree, octave shift (up/down), accent (higher velocity), slide (legato transition into the next gated step), gate (active/silent). + +**Parameters:** +| Parameter | Range | Default | +|-----------|-------|---------| +| Density | 0–16 | 8 | +| BPM | 40–280 | 120 | +| Key | C–B | C | +| Octave | 1–6 | 3 | +| Scale | 6 options | Major | +| Seed | uint32 (randomized on REGEN) | 12345 | + +**Controls:** PLAY/STOP, REGEN (new random seed and pattern), Density±, BPM±, KEY±, SCALE cycle. Octave has no touch control; use encoder knob 3. + +**Pattern generation (REGEN):** An LCG PRNG seeded by the current seed value generates note degrees, gate probability (higher density = more gates), accents, and slides. REGEN and density changes draw a new random seed; key and scale changes reuse the current seed. The same seed and parameters always produce the same pattern. + +**Slide:** A slide flag holds the step's note past its nominal 90%-gate Note Off. If the next step is gated, that step's Note On is sent before the old note's Note Off, so a mono synth hears a legato transition. The old note is then released so a poly synth does not leave it hanging. If the next step is silent, the held note ends at the step boundary. + +**Encoder:** Knob 0=density, 1=BPM, 2=key, 3=octave, 4=scale, 5=regen (any delta triggers regen); Button 0=play/stop, Button 1=regen. + +--- + +## EUCLID - 8-Voice Euclidean Sequencer + +8 independent voices, each with a Euclidean rhythm pattern. All voices share a global BPM and step clock. The canvas shows 8 concentric rings (one per voice) with a rotating sweep line indicating the current position. + +**Euclidean patterns:** A bucket (Bresenham-style) accumulator distributes `events` triggers as evenly as possible across `steps` total slots. Before rotation the first trigger lands on step 0; rotation shifts the whole pattern. For example, E(3,8) with rotation 0 produces `[1,0,0,1,0,0,1,0]` (triggers on steps 0, 3, and 6). + +**Per-voice parameters:** +| Parameter | Range | Description | +|-----------|-------|-------------| +| Steps | 1–32 | Total pattern length | +| Events | 0–steps | Number of triggers | +| Rotation | 0–steps-1 | Pattern phase offset | +| Note | 0–127 | MIDI note triggered | + +**Global parameters:** BPM (40–280), triplet mode (2/3 timing). + +**Voice colours:** Each voice has a distinct colour both on the canvas ring and its tab button. + +**Controls:** PLAY/STOP, TRIPLET toggle, BPM±; per-voice: tap voice tab to select, Steps±, Events±, Rotation±, Note±. + +**Encoder:** Knob 0=sel.steps, 1=sel.events, 2=sel.rotation, 3=sel.note, 4=BPM; Knobs 5–7=steps for voices 6–8; Buttons 0–7 = select voice. diff --git a/Apps/MidiController/docs/scales.md b/Apps/MidiController/docs/scales.md new file mode 100644 index 0000000..f743eed --- /dev/null +++ b/Apps/MidiController/docs/scales.md @@ -0,0 +1,73 @@ +# Scales and Keys + +Defined in `MidiScales.h`. The shared scale/key helpers are used for note generation by RNG, ZEN, DROP, and CHORD (CHORD always uses a C root). KEYS keeps a scale/key selection for its status row but remains fully chromatic. TB3PO has its own internal six-scale table; RAGA uses raga-specific pitch sets. + +--- + +## Scales + +| Index | Name | Intervals (semitones from root) | Notes | +|-------|------|---------------------------------|-------| +| 0 | Major | 0 2 4 5 7 9 11 | 7 | +| 1 | Minor (natural) | 0 2 3 5 7 8 10 | 7 | +| 2 | Dorian | 0 2 3 5 7 9 10 | 7 | +| 3 | Pentatonic | 0 2 4 7 9 | 5 | +| 4 | Blues | 0 3 5 6 7 10 | 6 | +| 5 | Chromatic | 0 1 2 3 4 5 6 7 8 9 10 11 | 12 | + +--- + +## Keys + +12 root notes, index 0–11: + +`C C# D D# E F F# G G# A A# B` + +The key is a transpose offset applied on top of the scale intervals. Key 0 = C, Key 2 = D, etc. + +--- + +## Helper functions + +### `getNoteInScale(scaleIdx, degree, octave, keyTranspose)` +Returns the MIDI note number for a given scale degree, octave, and key. `degree` wraps around the scale length automatically. + +``` +getNoteInScale(0, 0, 4, 0) → 60 (C4, Major scale, degree 0) +getNoteInScale(0, 1, 4, 0) → 62 (D4, Major scale, degree 1) +getNoteInScale(3, 0, 4, 2) → 62 (D4, Pentatonic scale, root=D) +``` + +### `isNoteInScale(scaleIdx, keyTranspose, midiNote)` +Returns `true` if the note belongs to the given scale/key. Available for scale-aware highlighting; the current KEYS view is fully chromatic. + +### `isRootNote(keyTranspose, midiNote)` +Returns `true` if the note is the root (tonic) of the key. GRID is chromatic and highlights C notes directly. + +### `midiNoteName(note, buf, len)` +Formats a MIDI note as `"C#4"`, `"A#3"`, etc. All accidentals use sharps: C#, D#, F#, G#, A#. MIDI 60 is C4 (scientific pitch notation). + +### `midiNoteClass(note, buf, len)` +Returns only the pitch class without octave: `"C#"`, `"G"`, etc. + +--- + +## Scale usage by mode + +| Mode | Uses scale | Notes | +|------|-----------|-------| +| KEYS | Label only | Fully chromatic; scale/key shown in the status row but not applied to key colours or pitches | +| BEATS | No | Fixed drum notes | +| ZEN | Yes | Wall segments map to scale degrees | +| DROP | Yes | Platform note derived from scale | +| RNG | Yes | Random note drawn from scale | +| XY | No | Raw CC values | +| ARP | No | Chromatic piano plus fixed major/minor/7th chord intervals | +| GRID | No | Chromatic isomorphic layout; C notes highlighted | +| CHORD | Yes | Roots from scale with fixed C key; chord qualities follow the major-scale degree pattern | +| LFO | No | CC/pitch bend only | +| RAGA | Custom | Raga-specific pitch sets (not using scale index) | +| DIMENSIONS | No | Equation output mapped to MIDI range | +| BABY8 | No | Notes set freely per step | +| TB3PO | Yes | Own six-scale table; generated steps quantised to scale (its Chrome option is the full 12-note chromatic scale) | +| EUCLID | No | Notes set freely per voice | diff --git a/Apps/MidiController/docs/units.md b/Apps/MidiController/docs/units.md new file mode 100644 index 0000000..00b6bb1 --- /dev/null +++ b/Apps/MidiController/docs/units.md @@ -0,0 +1,333 @@ +# I2C Unit Drivers + +The unit drivers live in `Libraries/M5UnitModules/`: + +- `Include/UnitXxx.h` +- `Source/UnitXxx.cpp` + +The app compiles them through `Apps/MidiController/main/CMakeLists.txt`, which globs both the app's `Source/*.c*` files and `../../../Libraries/M5UnitModules/Source/*.c*`. + +Each I2C driver follows the same pattern: + +- `begin(Device* dev, uint8_t addr = DEFAULT_ADDR)` - probes the device; returns `false` if not found +- Methods are no-ops (or return `false`) if `begin()` was not called or returned `false` +- No exceptions, no dynamic allocation + +`UnitMidi` is the exception: it drives a SAM2695 over a UART at 31250 bps, so its `begin()` takes a UART controller instead of an I2C address. + +--- + +## UnitCommon.h + +Shared helpers for **STM32-based** M5Stack units (8Encoder, ByteButton, Joystick2, Scroll). + +STM32 units use a register-pointer protocol: write the register address (STOP), wait 3 ms, then read the data. A repeated-START without the STOP will return stale or zero data because the STM32 HAL fires its receive callback on the STOP condition. + +```cpp +// Write data to a register. len is limited to UNIT_MAX_WRITE_PAYLOAD (32 bytes). +bool unitWriteReg(Device* dev, uint8_t addr, + uint8_t reg, const uint8_t* buf, uint16_t len); + +// Write just the register pointer (no data) - prepares for a subsequent read +bool unitWriteRegPtr(Device* dev, uint8_t addr, uint8_t reg); + +// Full read sequence: writeRegPtr → vTaskDelay(3ms) → read +bool unitReadReg(Device* dev, uint8_t addr, + uint8_t reg, uint8_t* buf, uint16_t len); + +// Perform a harmless identity read and validate the response +bool unitProbe(Device* dev, uint8_t addr); +``` + +`unitProbe` does **not** call `i2c_controller_has_device_at_address()`. On ESP-IDF 6.1 that helper uses `i2c_master_probe()`, which can report success for an absent address when the bus FSM is left in a non-error state. Behind a PaHub this made every known address appear to ACK on every channel, and the repeated probes could also wedge the bus so that a following `deselect()` failed. Instead, `unitProbe` performs a harmless real transaction and checks that it completes: + +- PaHub (`0x70`): read the control register. +- 8Encoder (`0x41`), Scroll (`0x40`), ByteButton (`0x47`), Joystick2 (`0x63`): write register pointer `0xFE`, wait 3 ms, then read the firmware version. Only versions 1-15 are accepted. +- LCD (`0x3E`): write command `0x04` (READ_ID), then read four bytes. The first two must be `0x77 0x89`. +- CardKB2 (`0x5F`): write `0xF1`, wait, then read the firmware version (1-15). A plain read is deliberately avoided because it would consume a queued key. +- RFID2 (`0x28`): write `0x37`, wait, then read the `VersionReg`; `0x15` is the WS1850S in the RFID2 Unit, and `0x88`, `0x90`, `0x91`, `0x92`, or `0x12` are accepted for MFRC522/PN512-family readers. +- Other STM32 register-based units: write only a register pointer (`0x00`); no data follows, so no register value changes. + +A NACK, timeout, or invalid identity value is a miss. Each failed probe is retried up to five times with a 2 ms gap, because an STM32 slave can briefly NACK while it re-arms its I2C listener. `begin()` still validates the device with a real register access. The 8Encoder checks both its firmware version (`0xFE`) and its own address register (`0xFF`); the LCD checks its four-byte ID; CardKB2 and RFID2 re-read their version registers. These checks are what prevent a floating PaHub channel from latching the scan onto an empty port: a write-only transaction can appear to ACK when the downstream bus has weak pull-ups, but an absent device cannot return the expected identity bytes. + +`KNOWN_UNIT_ADDRS` lists the known unit addresses used by the M5UnitTest PaHub scanner: `0x41` (8Encoder), `0x3E` (LCD), `0x40` (Scroll), `0x47` (ByteButton), `0x63` (Joystick2), `0x28` (Rfid2), and `0x5F` (CardKB2). Every entry now has an identity check, so the scanner only lists a channel's devices when the expected version/ID bytes come back. If a scan finds nothing, it disconnects and re-selects the channel once before reporting a miss. MidiController does not scan the full list; it probes `0x41` for the 8Encoder and `0x3E` for the LCD on each of the six PaHub channels through their `begin()` methods. + +--- + +## Unit8Encoder + +**I2C address:** `0x41` (default, configurable) +**Protocol:** STM32-based (STOP+delay pattern) + +### Register map + +| Register | Description | +|----------|-------------| +| `0x00 + i*4` | Absolute counter for knob `i` (int32_t, LE), `i` = 0–7 | +| `0x20 + i*4` | Delta since last read for knob `i` (int32_t, LE), resets on read | +| `0x40 + i` | Write 1 to reset counter `i` | +| `0x50 + i` | Button `i` state (0 = released, 1 = pressed) | +| `0x60` | Toggle-switch state (0 = off, 1 = on) | +| `0x70 + i*3` | Encoder LED `i` RGB (3 bytes: R, G, B), `i` = 0–7 | +| `0x88` | Toggle-switch LED RGB (3 bytes) | +| `0xFE` | Firmware version | +| `0xFF` | I2C address (R/W) | + +### API + +```cpp +bool begin(Device* dev, uint8_t addr = 0x41); +bool isPresent() const; + +// Read all 8 deltas and button states in one call. +// Deltas are returned in detents (hardware counts ÷4). +// Performs 16 individual register reads. +bool readAll(int32_t deltas[8], uint8_t buttons[8]); + +// Read the toggle switch. Returns false on I2C error (state is then unchanged). +bool readSwitch(bool& state); + +// Set one LED immediately. rgb = 0x00RRGGBB. +// idx 0-7 = encoder LEDs, idx 8 = switch LED. +void setLed(uint8_t idx, uint32_t rgb); +void setSwitchLed(uint32_t rgb); + +// Set all 9 LEDs (8 encoders + switch) to the same colour. +void setAllLeds(uint32_t rgb); + +// Compare pending[9] against cached state and write only the changed LEDs. +// pending[0-7] = encoder LEDs, pending[8] = switch LED. +void flushLeds(const uint32_t pending[LED_COUNT]); + +// Cached colours, index 0-7 = encoders, 8 = switch. +const uint32_t* ledColors() const; + +static constexpr uint8_t LED_COUNT = 9; +static constexpr uint8_t ENCODER_LED_COUNT = 8; +``` + +### Notes + +- Delta registers accumulate movement between reads - no encoder ticks are lost even at a 100 ms poll rate. +- `begin()` performs the identity transaction, verifies the firmware version (`0xFE`, 1–15) and the address register (`0xFF`, must equal `0x41`), then retries the encoder-LED and switch-LED init writes up to three times before giving up. The identity check is what prevents a device that merely ACKs at `0x41` from latching the PaHub scan onto the wrong channel; the LED writes are only a final liveness check. +- `flushLeds` sends a 24-byte payload (25-byte transaction) for the eight encoder LEDs and a separate 3-byte payload (4-byte transaction) for the switch LED, but only when the corresponding cached colours changed. + +--- + +## UnitPaHub + +**I2C address:** `0x70` (PCA9548AP-based, fixed) +**Protocol:** Direct I2C write (no register pointer) + +The module exposes **6 channels (CH0–CH5)**. The PCA9548AP chip itself supports 8 channels, but only six are wired to Grove ports on this unit. + +A single-byte bitmask write selects which downstream channel is active. Only one channel should be active at a time. + +```cpp +bool begin(Device* dev, uint8_t addr = 0x70); +bool isPresent() const; + +// Activate one channel (0–5). Writes (1 << channel) to the device. +bool select(uint8_t channel); + +// Deactivate all channels. Writes 0x00. +bool deselect(); + +uint8_t currentChannel() const; + +static constexpr uint8_t NUM_CHANNELS = 6; +static constexpr uint8_t NO_CHANNEL = 0xFF; +``` + +### Channel assignments in this app + +The app scans **every** software channel (0–5) for the 8Encoder and LCD Unit and uses the first device that returns a valid identity (8Encoder firmware version plus address register; LCD four-byte ID). The identity reads and init writes are retried, because the unit can still be re-arming its I2C listener immediately after the probe. The scan starts at 0 and stops at the first match, so no channel is preferred. + +Software channel numbers are zero-based (`select(channel)` writes `1 << channel`). Software CH0 is the first port. + +| Software channel | Example unit | +|------------------|----------------------------------| +| CH0 | 8Encoder (0x41) | +| CH5 | LCD Unit (0x3E) | + +Always call `select(channel)` before any transaction with a unit on that channel. `select()` skips the write when the requested channel is already cached as active; writing the same mask again briefly disconnects the downstream bus. After a real channel change it writes the mask, waits 3 ms, and reads the control register back for diagnostics. The readback is **not** a hard requirement: once a channel with an STM32 unit is selected, that unit can hold SCL low and block the upstream readback even though the mux latched correctly. `deselect()` writes `0x00` and does require a `0x00` readback, because with no channel selected the upstream bus should be free; that check is also what guards `UnitPaHub::begin()`. A failed write or deselect invalidates the cached channel, so the next `select()` always retries. + +--- + +## UnitLcd + +**I2C address:** `0x3E` +**Protocol:** ESP32-internal command buffer (no STOP+delay needed) + +See [hardware.md](hardware.md) for the command reference. + +```cpp +bool begin(Device* dev, uint8_t addr = 0x3E); +bool isPresent() const; + +uint16_t width() const; // current rotation's logical width +uint16_t height() const; // current rotation's logical height +uint8_t rotation() const; + +void setBrightness(uint8_t brightness); // 0=off, 255=max +void setRotation(uint8_t rotation); // 0=portrait, 1=landscape, 2/3=flipped + +void fillScreen(uint16_t rgb565); +void fillRect(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint16_t rgb565); +void drawPixel(uint8_t x, uint8_t y, uint16_t rgb565); + +// Set active drawing window (CASET + RASET commands). +bool setWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1); + +// Write raw RGB565 pixels into the active window. +// Sent in chunks of 126 pixels (253 bytes including the command byte). +void writePixels(const uint16_t* pixels, uint32_t len); + +void drawHLine(uint8_t x, uint8_t y, uint8_t len, uint16_t rgb565); +void drawVLine(uint8_t x, uint8_t y, uint8_t len, uint16_t rgb565); +void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t rgb565); +void drawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t rgb565); + +void fillCircle(int16_t cx, int16_t cy, int16_t r, uint16_t rgb565); +void drawCircle(int16_t cx, int16_t cy, int16_t r, uint16_t rgb565); +void fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t rgb565); +void drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t rgb565); +void fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, + int16_t x2, int16_t y2, uint16_t rgb565); +void drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, + int16_t x2, int16_t y2, uint16_t rgb565); + +// Angles in degrees (0=right, 90=down, 180=left, 270=up). +void fillArc(int16_t cx, int16_t cy, int16_t r0, int16_t r1, + float startDeg, float endDeg, uint16_t rgb565); +void drawArc(int16_t cx, int16_t cy, int16_t r0, int16_t r1, + float startDeg, float endDeg, uint16_t rgb565); + +void drawChar(uint8_t x, uint8_t y, char c, uint16_t fg, uint16_t bg, uint8_t scale = 1); +void drawText(uint8_t x, uint8_t y, const char* str, uint16_t fg, uint16_t bg, uint8_t scale = 1); + +// Query remaining command buffer space (0–255). +// Returns UINT8_MAX on I2C error (treat as "plenty of space" / safe fallback). +uint8_t bufferRemaining(); + +// Convert 0x00RRGGBB → RGB565. +static uint16_t rgb888to565(uint32_t rgb888); +static uint16_t color565(uint8_t r, uint8_t g, uint8_t b); +``` + +### Pixel chunking + +The ESP32 I2C driver has a 256-byte buffer. `writePixels` splits the stream into chunks of **126 pixels** (252 bytes plus the `0x42` command byte = 253 bytes). Each chunk is a separate I2C transaction. + +`begin()` first reads the four-byte ID (`0x04`, expected to start with `0x77 0x89`) and retries up to three times. This identity check stops a false ACK on a floating PaHub channel from being treated as an LCD. It then retries the brightness and rotation commands up to three times, because the LCD controller can still be busy immediately after a PaHub channel switch. + +--- + +## UnitByteButton + +**I2C address:** `0x47` +**Protocol:** STM32-based (STOP+delay pattern) + +8 push-buttons with per-button RGB LEDs. + +```cpp +bool begin(Device* dev, uint8_t addr = 0x47); +bool isPresent() const; + +// Read all 8 buttons as a bitmask (bit i = button i, 1=pressed). +// Sets *ok=false on I2C error (ok may be nullptr). +uint8_t readButtons(bool* ok = nullptr); + +// Read a single button (true = pressed). +bool readButton(uint8_t idx); + +// Set one LED (0x00RRGGBB, idx 0-7) and send it immediately. +void setLed(uint8_t idx, uint32_t rgb); + +// Flush pending[8] to hardware in one burst if anything changed. +void flushLeds(const uint32_t pending[8]); + +// Set all 8 LEDs to one colour immediately. +void setAllLeds(uint32_t rgb); + +const uint32_t* ledColors() const; +``` + +**Init note:** The show-mode register `0x19` is written `0x00` during `begin()`; otherwise LEDs will not update. + +**Wire format:** each LED is sent as a little-endian `uint32_t` `[B, G, R, 0x00]`. + +--- + +## UnitJoystick2 + +**I2C address:** `0x63` +**Protocol:** STM32-based (STOP+delay pattern) + +Dual-axis thumbstick with push-button and RGB LED. + +```cpp +bool begin(Device* dev, uint8_t addr = 0x63); +bool isPresent() const; + +// Read offset-corrected 12-bit X/Y. Centre = 0, range ~±2048. +bool readXY12(int16_t* x, int16_t* y); + +// Read raw 12-bit X/Y (0-4095, centre ~2048). +bool readXY12Raw(uint16_t* x, uint16_t* y); + +// Read offset-corrected 8-bit X/Y. Centre = 0, range ~±128. +bool readXY8(int8_t* x, int8_t* y); + +// True when the button is pressed (inverts the hardware's 0=pressed convention). +bool isPressed() const; + +// Set LED colour (0x00RRGGBB). Returns false on I2C error. +bool setLed(uint32_t rgb); +``` + +--- + +## UnitScroll + +**I2C address:** `0x40` +**Protocol:** STM32-based (STOP+delay pattern) + +Single rotary encoder with push-button and RGB LED. + +```cpp +bool begin(Device* dev, uint8_t addr = 0x40); +bool isPresent() const; + +// Read accumulated delta since last call (int16_t, resets on read). +int16_t readDelta(); + +// Read the absolute encoder value. +int16_t readAbsolute() const; + +// True when the button is pressed. +bool isPressed() const; + +// Set LED colour (0x00RRGGBB). +// Wire format: [0x00, R, G, B] at register 0x30. +void setLed(uint32_t rgb); + +// Reset the internal counter to zero. +void resetEncoder(); +``` + +--- + +## Other drivers in the library + +`Libraries/M5UnitModules/` also contains drivers for `UnitCardKB2`, `UnitDualButton`, `UnitMidi`, and `UnitRfid2`. They are not used by this app, but are compiled by the app's CMake glob. `UnitMidi` uses UART rather than I2C; the others follow the I2C driver pattern. + +--- + +## Adding a new unit + +1. Create `Libraries/M5UnitModules/Include/UnitXxx.h` and `Libraries/M5UnitModules/Source/UnitXxx.cpp`. +2. Include `UnitCommon.h` for the shared I2C helpers. +3. Implement `begin(Device*, uint8_t addr)` returning `false` if `unitProbe` fails. +4. Guard every method with `if (!dev_) return;` (or return `false`). +5. The file is picked up automatically by the `GLOB_RECURSE` rules in `Apps/MidiController/main/CMakeLists.txt`. diff --git a/Apps/MidiController/images/0_transport_selection.png b/Apps/MidiController/images/0_transport_selection.png new file mode 100644 index 0000000000000000000000000000000000000000..9df021a85e5bdaf3763efa48017bfc4c664ae2ca GIT binary patch literal 17865 zcmeHuby$?|*XJ`sHwe-xD&5_PG)Q+N(%lWCh$!8lG)T8}qcliK#{hzKcMh|oet++~ zf9$)vf9<>1?#p!zGd$CY1#QTO>M?z)`ex& zLqDcEdIm%_P1StDEHf(I>jz$s{)*XxGq4LPI`}yhx3UY%n8f9VEgX9e{L;$(_B`eb zvs)^;Nf@TGk4R{-QP~h1r=YB|mcF@-y?3BbXhdk@$5-_e%9;jlL2vnFHQqtbdHi!s zZJixE7YVe285p_T0z=GfoK!Rn6kceFiOUL!%LYZp=^DS1lv30%Fb|GS(9korb@w&2 zuqD@p*e8@TTYP98nTNyS`zPnZ&hawxYDN}zPCh~YAEKW40wR zrPucl&#$3lKS1SshHU176rKex=s+W*LuKGXc7lLYD?zm*C>mxc(rUoi0qENjQa}u_ z3@0L?!zCbNU=osirY9t##>OE@PQ|35>1<{7USHpvhfiMlg$*q|m#Jy6tehDtk2tc8 zFF2zXRMLh0fP#>cf&3{i1*5gSHN2udO&Mb*bM2(G4hFUcOjwv9e=b~Z}#=<2g z4in5(lfGK+d2%5%=h;X%E~K;hex5%B_(Bz$f#I)E(LZW4Qe)NCO#D%eP6j3 zc06LbQVLdr&y38>-zcj)i^-T98V9Otdk~Y*KceTxC!nCDV$sxek$k4l$RsQ*qR!4C zB`0U5ukXXlr|{y1O(Y?Y82~&dQjmV80`KMxB?{eJd@1z*@E7cL@uz~~^1NoN z)MPPl3Mq8s!07Ui1y&*?)UOCK=h6?t^WOT{9rC}$Z{7*uLD$Si$3U9s=wuY5<(}h) zqkl+mcSNW2$lsQIZTdPp2foUgHj3Gm>_ASlqu;JJxNL46D{@65ZQcZ!#+Z@9_;@vD3BRVfPJ{?`iDCPM%V> z_slpB)HONWHNR?AQDml<;zejRqh6D#i%tm`%^Xcle;|@<_(>zd2AqAE%KS9+^qt_G zbr3t_6cPkIC)6r`Oh_lSh1ftvQHRNp1LEXwg9vMT?nUlhx`I3a5J=7BN_WP0Buf6w zQa`x11ZsH2Yq*C{5NmhirsCO(SC>OUwbxFCqSk=q+cCa@Pn(Df9#>9$hGBP)euu)Y z?|!$7HfD$CFwi?yGod{t^*fCKosM7(h}Z1wnh;nLn*7~jv) zBP`S&&_<DXEZ(|Obw zEN2@cAwpnlu3i$ei=p5byL$q+s_A0t@N*vTK$Y5(t&=jvzTk*99ZMQfxHYQEo2&Hh zu3R=f8-0!6{EThfnnMn_Iw3RONj-agdtQlkC6ES-P`#?!qLEf-I|mmMNCm5uQk-3* z!&k3_pn6FvenaQN&9T8Xa|Bt(7xBw_4e{yoU(fU&rOBd?8|Yr&)h-1Ze4kRDac48_ zxRdzw^0cs;g5~YlRx$!#k6`$+_4Nli4}JcvDUsd+k~3%iH13OosrB}1)p)!2b4eD| zrqbo5-Rdp3wYklYM_t%ql~(T;cy-Wos>?Nn<3ujueGKsA%v7~z_K&qgvwdAyNIq9_ z)zG29b?nY;zwhM$^Mn*zS20prrBgJYpx_-li_({IumqCI2n0Q*e853l8 zjJ&e~1@Lp0$97ONJdkGTR^B?5)j!?m;V+lEfW3c66-Yz3l2{FM<;bz*eX>p9dfTuG z7}Q}rft+9hBAF19kM(&sul3&T%yu9H(p!Uud1MHdQhS!9+@*#+X)oC+$ahXroa7!@@1evl2}>n-^FO<(h~EZEI3xMo2Qos!0xn11 zr}R&qvXr(z06bs67*Xi%DB=dD9B7Zzsey9ribrhWk2nm`LLJq;T97+mx`7LkC0iaf z1-wJ8KW{qptd`C5~mbTGTwAp(G)60OfQ=F7Bn?W9mKA*^vFs#~Dlojuk=6F5>JMPxJAYNwNG(JZJ&p4yy zNQf)uwe zWD9=7dOh*z?U!cI-B*~1`2DdG5R`Cxx&H$wyAd$$r0m?|5y(f$)>(Osz=(*y+1Q(z z;XT&yI53i?UUo+e5eB4-Di=I{Y?>BA2>n{bx9BXKF+17^L3hHF9RiVJl&8c^!3Kiz z2ut0RV%Ib3kAadq9@dKRW)Sb`>mH=**6FuK^R9o)b<3{mxFb#ifruJuyZ6NKiK&=G z+voC91H72gjyxD4Xp#F-J|7k(TJ+Ab^Z>?{A8#otuBO3b1bJtBxxbXqApA_n8YgV8 z^#l6ky!rDW|L_h71shw8-y+IKQ$C7XQ+FcU`oD%n)1^2aJ4FuV@XQkwMJi zk>o9%lwdA2&VvUo5$scFVrC8om|l-h4{eHqYnUwXWLu^6u5{67TRH%>SOSzJ4jcd~ z#UO)m0}#D`FsfeSpAq0Jo}(9o>@pJRX1T%9>CXEQc*I+1=utD&f!ul35ddpyv-~@1 zzEKgsL5!MP@B6@_;J>40OcBlvf-b?HKroDi@Ew8jlh4|ZSmjXY5!dhA?Cp=L z8h(iNb68rN_1-{H3c^bxWSkPt3a_d{MdOTV2(zF%+J_i^HaF#O%>6PKf%&;b#ib*o8+r#pGFB;Si>IN;21?(AvBVjgZt z4{Z1IBy(zQvL%C@v(wJj(4OVoPm?JT z&>Nfj2`}@_CHQgsQGiHYT4)097gmuLbNI*!8Jcz_GJ0NIy;sWM0#q@IAR+qHudPKL zzO+slU21}k-)}TeMh_9Ob^7($X{oY?;_Q$$xZUtv7$R@5U#r&~98nA<7qb)N&^z2B zIrW|#$}*|+qmX1xV`59$TXs`S(~a8Mz!4FG<=JR{( zD$xbrPJL3c&Ew$?&WK9fFt7|OZ}$<>ftj?&N@OcWc`35=VC=$?PKIWqA$Gkp}hO%fLq4B+FS;{&-UAh2XD ztt9ZC7#STHe*=JM4e)6JX)G`@#O4_Uz-RlvKxtVj-pV)msh=_9AdVLoG<^|jvz)K5 zt!t@S#^QLb@Wnj?*UsnG2unuav(0z>anD6~)lgnZ#bvF6I5(5HFc(V{%|gMXLZtsg z4`JnN?Bb>22?^Ja6ru#kSGZrsHRkjkdU3p}7t*0n8~GAX$Eq3JHHP&O&)1y>&ck*1 zLWd6!MDt z%=LQWiPgC^$SQ5!_uC<+3+rP1ffI&HS#8TI3n;oxSNqA(%aifN5(87nPo1BdL(GiCR z-AuPon%f>B+io2WGxS={k4hwUvBT zv8ejimR>vg(41jc>M4>hf~ z+dr7O(@1%I{$ZS&u`&8sK<{cS2MS}a;X{7Y)Y9yPrluuBHw1`z2Rhr1Cvm;WBO?eH zIgmi&Szoc6pD0XBzTXi5*^wdM+Tq7xFP%04O7L>g#OowLTR5}Xzrn7t2GcLOEnk5F z>taTuXr`j!J}9!O?S;tCCIP6WD*BNnZRe&%w|W38{As6KOX@|SssHFEnDy7yD(RFe z-as9Y!P5RPyXNDVf_~Cf#&__UFhTD++*eC`=#w;Ao}fO8Kc zS{N+A&2aPI*2^&DiQu?R+cYq{3+muHl3XHRRlC-<3$IE+J|unA_+jiO2Qe6_-LVIS zd&uo_xa(U;sD|A}oS%wU)EF%r(?O) zs5@_7gkXV6f^IbDHx;l2kZneI(Topt`9$hsKTmDZC=F9wCyux$UF-1q7ZXbOgGHmq zO$^Q$uFEK5{Dp^cGSR^SXvG38J*{U60M@m7y0+*;v%`-vl$?u%+0at~(2YlFqT2_l zIaIq~_?hnIki99SkE@<=I5t9Y-O%gQHl)ErN9p z6(O<&18$nGs*p#ivM}1tAkTQFg+{UVkzaBxt#~LtM0tHH6?mr#`!B-8GB*EoGCEW% zH?~^B1DXR!@DIwY>sPaYN&@ObMW=U?fpVS>VzYfC`j=@L62U_&`vM&$LPZ|*olsJV zyM`3i`hYE0)y+SJeJC|#Sbs*6Rn&y7~C|`Nm@j&7Ue-pHu^c~IPTu< zjv4B>sX+XuSEB(5i&_tD|8!MHU0i?R(Ldl&G)aUlV{tBo=h!^ur%*vD>gEi+HZ3&- zQ|=9PV7M&lhdajCEkwchSQsSp+N)*eA=-=b0a zxkuceiRt4Z7@nWew2WLN%q97=#6!mnYX(eoY561icrYLEYzykw1ru3D4S!+ZAX6G5 zO{@j8e$Ui~1b;Z@$@*+)>^(Ul<8&mAzj8Vd689hq`(#ZHL~cJh&%teX2&)6~lf*Bn zM^J2-wHT=Owa>jT?9JCqR7DTQNmf{!&c{q&-u25}R;)f#)%`m+5L#6#A$kMPA4vog zWvh3lHdu8p%$IVxKZo=ZT+9{&o`#<=GsJi0GqA?`hZEJhHr^~X^Wm@DeW+jP=-xW9 z{cHi999fbRGhY9_?cy|Is>iYQmQNE7wk^Mlo{hY8t5q+~O*@apn_>+YE%JK~aksEA zr^ZDV)FF&`-&^6SduI$O=q?-6_Q878?Fzg16go!nd9dch!Jd^5>-jgs?GP`rp3oqE z6++e<@R3b4zslplDuY{#03}EX_vXg39QcevwUwnE-;5Lxjmd5flL3ldD$nN-;{q7MKz4kPh}a?KXhf;o zIh`cb7tw649VxU~o^W;5l)p8Z{oS&=P``qB`3?)joTSx@Z;|zIA>13WquvW2ffn(~ zPs0zF(K*RBXEf?DM{rIGt`LANn)dk6b{3oQZ%$la?_K^A4Y|j{f5-;8fa3pu&_BqzrX)AAK`CA|b1z9jyPnkaJ$dp%0L%#s9?n?ybvZPG8heC}( zigW5-TZ{}*2xIeLhJrX(H^wtAsGad$=KRPCeCpUMhudczv+T@V?J98O5`UJocqY( zBGV#onW5{US{-+|zuk?czf5b_Dy7rrR{i{=O0E&^ERz<6+-T2b-1O^!rFS@$D6=B? zhJ@Yg1G6p)f~=B5niYR@_inU~KM8NFP)byKxlT^Dt6a=5m~=EB$~uEm85|PO`D#;* z&n18JS77;PL`P-R0t+Fvy-Cd^nc7%uOHxg)cScg_!${Y5uPc#fCGb;dCJgl2iG`m^ zmfRad{($SvogalN8e@BaBJ=rNUjMzt^0^-1K5uAt9EHSS@iDrnISW<)0e>JjRLDG~ zb%a7Dps3kZ@uQ2a(cue3ppC*Bj4=w zbHb#ruj7C0JE%HDVT@@YX50`MoG$Wu#EdG_jzA~OvT-FzZ&OSs$c}`PO@o7-+t<|L z@o>M2cYz(PlQ3JY0g~x%@q_|KmDu~Y#I7oKw+h=V719lb*{Q<1my%7w_#8*kFeD7n zN!_Z$YGfYxm4Fzc%E<9o78P2_uZMKI8XF0eH@T-)VD2KFrMeBJ+eQ0;u z(M8nXsPiJvZcP%KZL0aD8?hw^CVC5p?$G9Kk_)MgJuoArldPP7F0yLhUlF_Z>~HKL zV2>US;?sWSx~~H==^`jBmJgT5sTMUDqW4@ptl2}QWIL+!Ot9aD)KVO zA}HhUpR~pDAN3@?6VKu4ATZSV%KJ@A{t8^lZ}1!vrP4e``Pt7+t=o=LyhkICK8D$|{p(O2v~j{G`8Rm|hj-yBB@v9wlVaC3npLa@@+@yFSf~3l z$Mj!8&EdK(?yrmp&aVqlW+|l!e|;g#BFOFv>MJML0}vAaIW4~rP* z7p^HfWZAo$Z$N(Eb|OQD=}dYF#;BS~yR)W(RVDQP(X`{8U9~9RK&Ym0=m{(`tCRYhQYM#;`*G(5ZM@cpkd3LA$LOW8s0TzunwvY+j5;D$ zeBMTINdt(EiMWp>Bzm{Tbmz&Ae|H4o1pFt&mbMcG{|H@{OI1Hn(`36~o`6g_RW4WW zbCWSGYk&1&xK#E*pGoXnoD9kdeR-AgRWEdkt`oGe+ zSt`%)L|rQmUNZ(u?}VvmV)dQlX<^2~O9|DrZLwm{@d@pBlP@2%VSFb?cfoj%J(xPc z>aFQ0#Qa{pf^~`=*^;osx1%B1!))F z)IB+xr5WvL69($O5s4YXp&cT+w~oSn?H|@mzkRrU?GAw6=;sCqXaFmRuYs?uh{vBx zV9=XQ0E}FN3`QOWA){|7d@As01v*WU{>KFUKVTYOGtUGEk3us9`8nnKB4&ddO8Z znHB_I)d+na%(Ln;Dn2057toT`5tKF}Vsm;sqdM_X9B-G1_HucRX5u57=e;Pkb!QgI z(K7aRV6_TTg$6{I)ef%!32$DsYSfMIeBY~Mr9=PZc4ow*V)QFH%GJaBG=@cpXqkXoH}x`t)Bx_I^5pJ zY!`)y-!Cp)OwBedKtHMok#0Cy3ojH}2ZCGgUM-+aD4@Y89d0Mqj~ZI8@47ttzj8{5 zFQ=+p4(Mn7HZbln@)aN9fbVYK%e#OFx3_WO<3B>jTQlJev-cs6rg*_EFp>!ZG>d?_ zwBN!Ez;yPaZ96f<{J`Q0^JX(w7p^|33VmP%AhxAt>4dua7=0> zq$=7$QbL}oi83-D!;LRZ>U4ZFCCMe+;eeVbP)iAvpg~qa+3SFs7(j#vu%SVAK-oTk zjS!%P2lOCG4uP`$07h!S02i1-k~{{++<+o#ARG;nj3jyfUnc&OvGC`F{H(Ge>gk)v z<1s9CuNHYoV|t>=pMt$Blhl=W{7RTCX7z?8L;u8B$A5VISd+Z{*I6Ja_68_g4gsa3 zBo&-Ca%W`D8DvC2?)i3pUOr#@B5nqpT&Wl znJ18ERp`M;o$8+48QL^%K^VZcH(2t#8HH0m?*NqUZ*;~1qJ^mdMJ8xKK{d-KwhWX6 z3XpP_qSN{2W;gWuRAgi{%FUg4?Q!NXmEN;hBHf$hfD|ZbW&0LPdk#|TUC|TT8fIVH0N3q1%S+&@zvCs}pUOpq@`0YAIXQ{~82R#rOfON+m#U&dimMq5;(Sy>}vy%Cl9ht8idV=gyW^Q z_0WH}3GS@6Upt1o-0n4k?Pdo%D5_fI2wVwwpne41;d=sa@SQhTu1QT#WO5JV)+ zfXl%rRyS;`C?wXGD!|hiA6q z+gYq{%TGP+rOffirH?BgWSbryKJ}-!O~L?NRF%&4I)}Fch}pP;!%Ux*s5r==Fe$waLl7(&}x^{WXR z*sey5lMFDakn00H0#}#sqr4RC>~GxcrfEI<6sH~uWSSTW2a^F_^E;zi61#&w2~lu| z!DBDj#eMyfQ#9^g>4*U)V*7|X5kV-uHDvQ8YZ*U!#K;GP9W3(Rll4TGnR)%L5@6Pr zrp?4zm(2N#tFw0GlAlag+TiQw!OPg>=#U*xjFtWmHD?dvkn6zApL%t9PyxW(8DCrnR237kzBaQ zp1LAY`2Mu{GJ8n!(l^I7d7?@ zTh9P1=3)DjNGtV4KZTAI;K=l14aV7FTsgCF^V*pICg?(-78{r%1Y$q=VE{8>ruRtK z2g5ZTIhKDI;_?9y)M9#=){BHYP-z`3weW{w4xa#fa8C(uc>O)nNC)&{E^7?YbO@&L z#{qOjE?Z`9{cdN|DX%tf?%X6KGGQp?e`uz42B_+|Szx)jtDt5yl(>aCqyM2J6XedI zU}HKLZED7aB<@cbe=njUF5Xshx-5CoKP0yVC+*ax>MR>BOf#9m69g^)2E$(;fLj!*d?D9O=8Qg!PO>23))%4W&_(dk%;@uASQygZ++G2wx&q#p z-{xTFNvA6}a;?GM2@d%{Ye!2^qu5zPu;@Qwp=NJi{M>9P6}WIOS`_)myDR|D0J1ND z>=VE&^RGc9h%fFRMDTzOp+9Ryfz(Zek`_p>d}@cA*N(46vy|_f!Rhhv8clPHPSmFG zXw+3`1k5|XIg^rg?M(WLity1YRhoMimiB78^3GF?X*iX84P1B4{>0i1b3QT+T0A z7X$K!eat{i-ykoiyFai(CC@LMu7ceW^wXfzn9v6g$l4{(rNja9|j`Nmx&?! zvgJ@qIQg!-ANWr-BV7FC>6t7?Lz6_(Dx4!YD=C}Ht2yl!85W0cMh^I68@5?!dx9cM z9Y?k?{+6OAaPdA9`W1!Kp(b)NIKG;xJ>SbtncCC9FfYBX%Cp#6rxI`w7uTNHoJ0mr zDG?PX48WD5{J5t~4_`%JBTglV=4mqmUvZ%P5mN(N)PEZTQ1iV3D6zxhu=9S79bkv$ znF$8@!s~vzR5h82$%)3qi=ZWJzdE%!jrhVoaDx+k`xb`wXA~6Tb<&M1qh`I|e~JE~ z;A=Xg-(?-j6Vc)KHq@g^BB#@x;w!|b`fg4~8&Ak!=Y$tDz$C(JrZ|w7%Hb80Y-QH{ z?yF5*0GyAc^KHp7KSaF7P7+5Pcg0k~9R7D6Y$I{%#W~;W8??MJbD9Yr0F5T+0w-&q)7f@@qS%2CK8si!Po4m=cifafD zZhTSoAP;s0CmObtI9S1YchH-_M#>)Ne2F#SuSX_i7wK1@k8K~R%&kwF3Ff05Ryz_9 z_UdB>@~BV(`)QyMe&bgF>Gj4GFJ$CN+QOP@HGGiaG2VI#!UpZXY{YJ-MF@@itZ89q zA98F=(CWp(zZiW`8GmELM|B%S-uc=XXYMO25OQp#>BgY|6)#Bg9qAAZ2mOL2`8Imu z0|$?R_ZMlhz-BG_nS1*d?0e;O_DDkVJC^ZHKycd zeEGsKS(CK5xm6P(!2}u};a{+20hxk_FG4=h^^wdy075@C^*BlLuKH4rG687M=?#9J zQhfxz*yAE{*8P)8oBIS42IvPY)!z~05yRn=}EBr#7PN)e+7KZU>)T=&$aPXRtx6Y$p~Ni7g&&%{F-+NHP$mj2oO&B?2HG_N~ZT$3-S**nX;yQ`j;M(0G0O}Z-!!t zSxO^+_b+GsT+AK|E<~2uHy*Uo$sn=+a~hHEuw9)p*_{-YyIbuiSwl2Dg1?9<0`#lz zw!fmd$mix(SyoXZ(+5*X-rH_1fL-{>M>h7BZ30!?5J8VL6GYk8-y;VJ#=Nmnj@Sbzw%jKdWO^Nt!`PbA} zq<_F7?0hmlNM!_skPXR4dA$-q47%fp$~_l+@8y!^Kd_a-l6TfM&K`6Lr}&eWNe~F37~BL@`2=Y&4{9;? zJohnRp5yZ-&l)oLi23FHWrUjy(^L+;)%!2ua-ad!DcAIEgCFA#yAii)T}F7D&uCnW z2Dq>LGJGl06IROfrY4>q6>XAp1~^9%mA{R4G`r>fRf4@s8Q@@NfBDO#l9}X3)6`LE zD9{x3aNF% z;wj&dTa+p_5I0@@vvl?}ym-C0j1LlN^u+3Ts-)`tWzK3K%D>4yhNqe~lfIvHv$HIv za6YS88F=LWki?=dGZVp!mA?rEseepcy7=W>Y5x}^-XV7u^Tj||dX|7* zD*&-EMZmHSNLJ{%B!{l%NZ15}Iz0a;mKia^p8nUfn_ z6b*#aL*fV|J^%kRf%x!8?l3BEA#cuiX1`)%=~lMmI_a`KOveZq{cdm=muG12HSO7Q z9Cg__erUDVc9`>!q*Jpd>9T*^(b8+)vt`Ks@O9qudH&xus^62B=;a#n=M-l4*~t+S zQ}>^L*Njh=otp?X*IG>+Ebv_}Vaa9P_WDCZSU4a_bMyWiTVMD~o1JwzS{b6nBj*1K z5)5mm{WpC~?Y%^{+o5|0bN*{v7XbpNCYLLNSSv-`hV6bFRENICpR@xTN=bhEmDz9G z|M`%#b70b8U(B=B+L1rw4C*xfy-hco{nTpXuW&r%V|i;sbg-c#-sxCyOQF?Tn|rJC zgs3lvKb|7?N>MH(+zVD3hS8E|H;*X@3^S% zp|5E;&Weo&l>F7APD0GGlg={RL7R1qz-2^>-$vU;8cSgYd z@Yq_Qqsi&hn^Rpcpg~1tbfm2>3wk}bJutd>u<90Yj%H*W#lg`k&}tfrh7U9oNCLM1 zsgF(w?mc|y_-lhITtxiho=f-w#=Vp|%EEyO!rr_zjj0!>Ie6gXbGC3~dbB<9HHb#| zilkM{B{pUJbOAR;9DYs{dNl{!&zZC_>W4ovrwmr4c@{0u+8*5PrcM`EvcHrWZNLEU zNEKvzqhLq0e6*~pA8<2#VC{IX0@P0j-P?@^2ZYqDNE~yu??wT4B#!`7E0=)=zNqc8 zeRN6nSfJaP9$45`4UuXX0tX6O6`=yAih5eH}3JcBiR_iVAGA9qQi-2nf za{@?ijyu=VJ0Kusc>WYUW>@%zN5a4wmtnl@e*R+EqU!Cgukz)Dud$cJQsw85-#j*z zf{n=EUoWZ5c@-BKwp?r9>Kz2=G=*%EOpDKDA)AJU(brFS+#bZQ{g| z93N{UJ$w$dM_^?HY^ki(dY$^-Eq*k*-4;E&?(EqRW_8!An{P8*{N<8-mF|A~k)i8n z30oRR+o&AJ22@>j!BVDceo0I)?d4|8pf>NaWc@;-SPI`^g$$ug->OCbC3X)i>Y!S^ zM7KR)ZXCu!AxyFsh~DX~e`rB-HhmMaiJPnR;p==1g~xP#Pj*dBi&IQb7 zD-Usk`Nmd-l)y+#E{^)4@aEf6Q}TkI5#6M+dr!7$l=TGP;mJCi+$}*kX^%fH7nZTX z$3EHZ>ecm#P$nSaU31obODFt20>L@z>#7*0m62kRXgO>-LX~DxR;Hk6k=Wi=H44p@ z*1OL?UYaeaZAPF3)y&fT zuAKL+z$0MUvNhz_8eW-TYl7G}<$h)^m+u>EgN?UsWr7R7Jho7Jq?2oI|B74~*-%DC zL`DZj=a;U&gj}ngn;G!Y@4N?PP0{|{oT*jA?G~IbLBebGsJV#K^@s`8%6{#9`sLrddH|nqBpyFfoE0?WM?t_Kz#bTb(WoF=b&u6i!$0(WcnM!Fs`C+lYHGsj0QK~~*C9W7IGuIB z+v$v$9oBmc-dEH36f&gPrD;MVk^^XI64(C0%MGWS%+^NV=4{h7HEN*;8vDWsZ;R{? zJujn5g}q}k4j+GzS+BEcIC-Vd<7JE?IqrJT_2p93S$TnkEi+~yB}EK3!CB;9oWkzY z0ImeA@AWdGBI9g3(#bkVQ^!9)3)&BhaoZ36$PxYyy+7@N<79ZfX?e?`s9myg@XRrZ z>Mxp{`IqI1SK)Kc`8kK=-S8)1{9yLCQ&@Ij6vV4&pj(Lma^;RpYZ4B0XW2LGKjeE2 z2BAx`f$<>*$h1Hmx+Gvn00AZ+0iYro0IU`PAZHW*mx9`^gqg5)^h0k#)!+XcqadRq KT_I)m=Dz_d?L3?S literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/10_chord_mode.png b/Apps/MidiController/images/10_chord_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..f33dc1d32fb969c2e2ad360b182dc14bda43aeeb GIT binary patch literal 36999 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 AssI20 literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/11_lfo_mode.png b/Apps/MidiController/images/11_lfo_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf20731fcd113c4e8c9c1c32eb16728201ba9b9 GIT binary patch literal 38735 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 literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/13_dimensions_mode.png b/Apps/MidiController/images/13_dimensions_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..ec3f7a5920e08770dbd0d908a7d6bd000d8b47d7 GIT binary patch literal 41764 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 literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/14_baby8_mode.png b/Apps/MidiController/images/14_baby8_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..1b6f8ae9cc961cf69ba4d6a95c2acfb0400b5c8a GIT binary patch literal 38557 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

KfP=v-DeoPS#b0ftk zGUl7ps(Pa}6TPt&HP$YQuA^TN&LZbwO^g2V*93N$~Ar1BW~*H zo42W&ogO_>o|2ZX_W$w;G@O_+Irx6?Jj`s_>$nAH;sd5{oDv|QrSD^mlOU>qe~kFK z#pT#7I-U?XI4at?$~Tx6ak%6rSy@`L)8plfCEXpx^9XkaXt$>t7eK4)V8uGZdLDy#QJmU8w zB*q5<4*pM8g6U_Ve8qB+AyO!L+ma3FfY;Cs2z|M98N-=dO*xmm#P!_3V{TiM&LYf= zwJ3wJaH8pOd?!a=@49!PZiNuRZ71-qgf%D9#Fs%w-gw8zPH)YTW6OQ7dB+_V-vr#r z(@Dd^CJ?WV_qn6M^k(+`5o0~6rqhk*`d!&QI&N@Je@Fs-*je4zmg~#OP{(Et0pk?* zTB(ZP9x(2DDRseVi1CrH&qYj6v5i*U+Yz3|#%6c2s~_8e*sK$$bD(zj`$H;E9oZVA zA)5-~brXWGhs-R)xiC_Hn%;T?L@_B?N(s}Vf+gh7aoyL(y?Y89ES79Yr&LsJDSNd2 z(aV*~PC($D&J^J{tF{*S7FUT<>A5j8uU?j08G*q|9QU`ZMfnz3gPkGT!rmsNcUc+5 zmz+P$5;u}v#eVlpBrmsMYVhfRT4{{j%&T_2LO!{w+}k+-l2D_2=Juvt$l_a$;46&M z3~u|`7iW)C^d2nCK52??IA7s7TRZ{ABQ^XKXwNmT|P~ zP<5>?&Xnuvv6%0=&ODtP>5qI6mqCbbEe(UvYm#t1K2)QN%YXyIV78?=j9It^Ie&OT zn)X8SQdUb)U0o@`oty&aaLbN;q4A-o*;Nddw-ZkGl0Jrz+cb=vXg}nGj@O;8ZEfZU znYEfz63q$RU+MF@C`lWjz`tiRS$`N9&bnUPOV;Tub*UGYduiwfT;A&ur`dOsg)qYY z;N{swt#?NgyV5>KYbs4=hapq8jZQuJ`T4BEJ2B)zzs`E~?Chl(IYHK?sp$K>Yh}#Ag0#IxW-kGdD9g&m4ep<_{T$VS6L9?@ZGnVZ&4HgTB`8 z&ciVi))RnC=fd2>wBwefVzl*1pC9KB4H|8Ky?{~M9k)#iGx=`AH(;|W=XJ2@Q^X=B zUlf-8T|hv<3deuTxq&c}5Zr!Ni=I>9*c>BhFht0SPR3iXO3eR0Bn^Vbfs_%H;)dYp zoIj{E!TDmfz}NGSgAo1b;siP(b_2))2%o8Yjc$X{5l*h4A!n{j^;q^Blz`f zZ5_5pC;`L1JB>Bk~i72qD1J9~8o zSuM-^wAUN`$5+cUfw(rcJ#GfrtSp%=5G$dKmT=+vgLVJwa{CDv&Z{}Q zpb!q>(8@?u^;wfe=2>tKXRC!L`QiH*BHO7|_b{m+e}MZhRlXew?}E zwdH#i8$m?IO+17IV4E~Pzy+R5Qz&FyBwC*3RD0mht~N-Bk*{v;(Vpgdj640>8; zeU8-+BdHqR{Qb(WeHWz?ss?G*(RDTAW>{O%Z)80J50 zwxuth3_ZJEe1yP9JmfETcGeKQcz!JY<-^o=h8w@N^U~Vf!0w{LZ^h4#!y&iBYzkdQ z%SdnCK=u4t z3!roGE9yzNw_+-Kl;4k&NjotVA-^=)9R}Uun6%ZpMI>)4zU!CS9$X(>c?Z>`T{-t# zyplQ)g%6%n+;=ZYY0w(0+On{_J{ZcJR`mVZ4i&5#BY+FNc1)yTBtCxLJf4mDa|YWb zpUyg@753?Q^`c0eTtUf%9_LFM;}UyZx0Dw_Gu-J`(-B0rIr?m=@U>1tbg)Lh@FiJl zqzcFgu+hL40R0zU3jwz~D&tKEHn)Hf|1u3uN3*`|!@#=8Sl#E22$V1nqF*Q8imcdg ztD!=n-DEj~BWG<>TSW_ChpYpd3kU<{Kh-`sq$E}{$bf@4j6hA@ExCk)vOM=Wbp}6t z=xar90Vb*(dV=~QBr^(ZRnFpaA8cjN9YWK)WM@C)1N0VaG3RxWTmhbB9s)~mkowpMMfG0HFonz0 z4dLOyTmlC>WKZ`?Y&#iBr*tcu4{S)zpU8IAq=4VjckHbjwnOPwR&>cTg`XNhGeW&U zTEQTGTND$G;Z) zkd#>II-TVoJ;@>U^j1fph9^r$@{OoX1tn(Y4uML?_ybx-QMc9k#Xg1G;I5Er?~kds zxuP2!!#26AN~`HtzZ!^uyoQ$Guoa#7rdU*L9o|(Iq!a+<<`+7g&Iv_e&`)-Buhq@nhtDEx!qHnCPnjT3HP^1&mfPiB-k%xno;? zlj@Sdc>bxUgD_7DPRIOH9z?_>qn$I9<@+Hu$Ci=a;cKLt^Db;WYgT&wGd%&Cdyrj)@qFyg^@d!e@dt?`jQSmFFK!rj0%d zM0KNNqw4#kA%FhIz={Q$r9MVrX09Z7xWUm_LVY&RE$IFQLh&WGkCHXFFdprVIB(_D zooo4YCYfkW$M6yOsLEaVH~V||Y9-ogaQyCv!K(M5PQ&wCYT-K*DmD?$FSH#K+Z*mG z^;Z6gse4FYKvzEM@mO>&;&%38%}IXf)04tK>!*9nN#Aa^^Sw_FcNGNCv|1>!@mPPr{tR^5ZuuT9dar~bN;76$}%jI zo2I0ZJ}XA{5rTsf#@->ppT=sRGba(LnA{A_csRe6xU>}v6n{~HCjQ>K*DlJ;iUss# zC>K{|-Z*%o8;IXGCvtnuMzBFWte9J%WCvPQD8{du;^!j#+KQDPH=aKCc!$JlQ`nn& z(vUT@%#Jtb>J`ZloG+5waXdJSFLECYi|58D38xo{H3TAMkcqNqx{*N?S0ShY z2iHy3TGn@KR8(0~1mXB?pTIO;-zlp(Ovl7t@f1|qRtK?xtyKvIetjD$fAu0t^Z4i# z$;cs=TgZS^XvQG2aSarTHWBx#7+2DqZESN_%LLBb%yhI1f1``O9L|q^h)t}7 zo2~%f!b9lF(aN%sQzEFR(8XgaML4iTEkgi+dwVeL?Xf$YY#SF6Iw&K6uD zdx5v_@bI?`=dHbz1Fs-Y~Q$E5!!3*tck+Ett<>$ zTDuZpzwXdvn*K$Gp>VHhnBtuoCl0UTp#n60Wc5&INrZds36?3AKZHq@PBQ9dM~W>* zq66GGX3zj^4-F=s(emKWg1KGX_!)I#eXPJ>&!GznmXsA&yCHiX6WUDqJ@c}ynbYZp zC?{Fyw6YIv!)BCHL7&`=pC_sd`UWl%FeqQY1Cv6@fAMi( z^zq>Rk1$GK#oo0JcL$-kFc-I{D+*>T+;vu;Vw&$a4J#?VS4kvjx0wuinXxncX2DR@g2m>0G{s`RdH!*XS8|8+6NLwItmC_}3^`Pn-&#d38s`b> z#cOd1Qw1+aYn=nV8fX84_($ zlFO+;V>}NBJdSK^K>Bn{F`&uAr|l~~WNX1I_W`lD!sq`&6v`$2hadf`g;nj)iRn&5 zI%7icH-;z5SygBCKB1)TE0ap=hC(5fPB(LXe&ZJmQ9>w~4m>uQ9eES3#T9wK^f=I> z`pqwuwcoeol#fT9(A8m!Wyb>SdzbVfx{l%MkO;_g6qI)?)85~fh><`}LAkAR=S_N_ zj$PCcnz5+{-9JOjm$etV=oiodORM}x7WA)ljvZXu+tucD?NEG$7$&)^kJ@T#)&60; zdVS8W&Z~|daEcwil!mCeqqCng(y}Z_NH;K}+|Cs9U`;G4Yd;Kmmp)S|Tu6Hl4X%W; zqjt*VXayZ(xaXyEhN$~R^b1hfva!iLlq)KkobhC60UrsXyN7KyoR|NT%!ISO)TAtL z*Zm>c7?%B!)F(@)Z!rVWU3atws;khx2Goqe^KeM8M}sKqqAlROY~9oeoVMx3M`xB} z_z=C;BkDSXZNb4O-EDi*1BNFcl&pFRa*oHnEJTDmw%sBO%V?(=Gp|_8NT+an%}ch} zI#!VX6KbTo?zz%FkK%T(d4h!AwwM*Ew-++8-=|4Xm442a7s=R$r`2HYJ%jbQv_#R&d7x@3vC zV5Ts~KDCWdEB@Uf7kCi^l)jz6T0KZ=|JUA1mgOR6> z{D&5ua&mQfy{73S^7_e9T64jWPVb-IB!qvBFL>@C95gShms^nCa?liMe3-A2ub!`! zubXd>ZvOjq9bq`1plGW zmPA^qo;Joj|1y$jruYaXT5NaoMe`-{rSoO;<@1$*Jm4QK(t$VTH&`?jGUB(dTs-lx z@ZL5l=!9Z^TzaX)M$&`b`0-{dolRBMTh(7RSTzzk2js^A3%Ll$$=I+!4x+rB2!$Hu2$dQQ@|8Ojzbk6xQb#is zLiE%8eB9UT&MjZe)o63?HB72_rb0$6oGiGKs%Ga0C1g6ksZ2Xah4^qjnjtD~(2r3q zffZVF9?$BXQ%)^R6GMQITa0gm+xLN*#S80>)E-Y z(b&5EW!`lVRK%xxZoD_)1Lzx_$`J#F(1+-k=EO)HJ#o%1R@FZFW$5%2?K~#g=zYx$kkENY?a>In z+LI9iHKGyXzYF^;>L!CR{3fv%=(fPs8e?Rp zM~s!se#zLkSKX+!9gjBuIn_gm@ZRzs7+NZXI+$imbK6!hjiV{GFgm<d*8qvF`Z;`AC4__oWq#vZer-zg{+!oU00X}U&%;lJzJUbJ5pVOO0(p#q=; z(22NQ9DfjD6G{tSZV98|AW8N)?Wr?=Ap0jN^)knvf8f}*%y;nY`hpHyTSK_A(tDrY z+o{Z#vVk1S?dV*=k9}iyecvw8aisjCQ)Hjg2$Sz|UsvV#4Y&5RB&XGA3#YSQf!!53 zTpQ~}A5=CjWO`yg{zOrTVC}AnlTT=UdE*K(xP&@jSo?3~46GLRE76ucirTye$|&y+dV-TD?zdz%pbSRw7&ylcS%dv%aEHpF{+O8HQ0 z+_0;wTKeI*ZWU$V*Gn7(sin@7!% z(68;3uMDg~lmqYHL&lnokR^GJUlfvp`oHylPYOscc)uYm5bTTU=Ms0aXQ4oIkA<-d;Oun zVkc%SUbx28Wp~jzA%0*pJ)H69`kmt;KN(P|qDU#WvRyrlDBQXyxp#UuzqGvZ=tT|` zg72579Ilk5)Sxt?w5^1uOy&@TN;swNyMs8m8s>oye~(E2Za2I^z|-gBzVb7UE ztS1`HPYW#XGtfOfRAM+k-9_N|6g*i}wAhFj`ZnXre=X^f7#lO0Nm z{Ys+QG+Tk`X$Z_7gFW!=^|u8>Apfs*iKaSL-lQH;f2p3POsDI~qn?AO3T@{_>uz^V zu9)|awk%*nSI^uYJcLWgrZGI7M{sBVZr=`2-+HDW9 z_(}kJJW&l6PzavLDQ3>1V*TkLZqt#CXCYX2@P!WbqUz`YVfLb;t(N>gYyJ7Ef5ZDz0$usVZ8Gx038#grkTmQpBdcR;p$G7K_}252T7gSFzNg)wom;c`1<8-#*E3swphr)wLrDg6`ICSkEvbZ)w>CpFp2D+m4=hKh~7)i zeE_6CeM0YMD7Bv`&{kKaJL)-{W`z<|=<{8MESPcS z#AOHi56#f~ez&0S_^(ybRPU?esS>D?sZxQrOFET!q1CH#X69^#^V5HN$}n`D^o%(g z><}mBU|x$cxHh`Vq}|rm#7pO)X?k^L`#|_M+5{UdNrZlKf%3kSok(Eq!k(OO-Cp4J z;Z|``iVvDzV} zhdYw7e>s9`p{tHb(N2{Lv{1Ft60Ozj`gN0-2HaIR8@b}ULBpEP&k^aa0VZwu&ZC2h zbtD#9SuaZ?r4~BA2-JId8vwoc~ zHqYXx2&(kmTVfZMYd5$mo`Xley8{&<(Cu5e+jGDdyHp8^rDbh=IbQHreLfv& z#G(W93DP%9^=RD=)wcUGBDT7-8hO37Gf&0(U9-lshyU-*P?YS??BFeqU|f~6F|IMK z{L-)L=MsIOyN2{1=BBo--?s9z?)LZ?WqatRd!+arzq%YZ=@J4w}ZPKr);2m;^r(~uHb%ztg9QlTvEVvFPIWX z?coUsZp*{zNPVrM>WjfN;VTYe3Cbw7g0Y4lMy5%J3u{Cd&t+8 z?fnG)#Z(S2fKm5|$d)!7%aSK8FK!t8$N#5xu!;i4RjGvzVueQ@2y7^ z=>_A5n^i;QHm$NJ3#ZdNKewrCR&b_IDAxMf-fRMw3k>9Cm&wznv^r0&cMo-};;5lt z`9fUCNu@ntw{+{yx60~{ORIz+gTePYS`CR_2;L>$q7TDf+);lXt2AU0gEeoOZS$3r z9*R>AjeqrCLH63*qvM;(7a59+s4F~uK?~!rh3wBL>TJFx)5oil$eB$Dk(HKLxm6)p zy;JrvpOHYn$)3%u`=oiStW22s)}0CUT`FKQ_o$H9@4<4e6Xf9ydn+DQ=!)dVX_rtP zE zz$uj06@G&1T=bDclb72jZenR2=5DB8Tx!rF-gKp7{qr797ctm*t7OOF ziG%YYk419Ca&O&5HK>w`$cs?Z>z+E}(6MW<#z?-HBRIbwmpF0Q^0VZ;s6LCMro{Dh zBkK5Lj>3=N?K*?Y5kZpJr4`kc=JBuFUR&6#v%73MiIttcM@ReCSx4=HmpqIaoZKqB zY}~3kd4gTD^!FFG7KW+>cD4#}eHQNa4|%UpUYJag3lIBoOI zJ`8rOx{Z<1u{+kZH?9UvC^`LQC)oFdi;vxH&GoS-&;I*^r%t~Jjs>p9Gmm{2k2M!d zx{iej-52bxjuH`9i)WjHGQm@CFB6;sEGZz;xmqU1d-6#PyQImuYK8t%e=x>0yQkpm zP3sdns{``7J$BKM$eBjZK)g7%O*mM=5AktuGbQ%slFW`eQl3#7X$F6WZ z8G&YIV_+Uel@muzr$_f$8=Xzu?;Me;7atEh*9mbcSA{jh$9)nLf($qn0;&4gHjsExt8t*?fOWVS>pqYTx9A21B@F)9nI-N~j@BSM5 z2=kw{Er7fb*lSO_Ugm&Z&NjHgTo$ptRMi3%hBblhY@c0;yID6OEm6Zozg1mx757O* zDI7X=nv~+FFiEOK&-D-tyVIg<{{WT9PQDdWPxhAt& zjBBeWWjOg7wYspLL@?`CYHbn@(e@c2`mD)@?$_$+ZKue;*~}skmp+krkyyX`QLwoF z>}>LS-B?;t1koro3S_J{)Zn4|JX<#(>J@>7nP z+e~pnXeut*z4UBR`aGRXn~fLQq5U!g53L&xjMgx*=K(JI@w9P{^m{fAQSH}vs}>2j0Vs)mtP4_8HX)1Wn=MMsC2fPj3yG} zjSC>E5rlE|i=shdn9l+pph|tcO)(t17jZn;#xe1w9un98$zF%QLQcNkO4TvDp`C}i zB3zcK<9WOm%cj{^o;!1iw+DHO-O9(sxN1ErvH0o3Ox-Z8|Kt}0DxUBh+K}jnib;=e zd-7QI?AE+zpFjFe8%n^^-&5Jp*Cri$ho9bW+2tkob6Ab?W+>x}zJw2R%nkK+t+`X- znCX3#nD1tY`Lg-dAM?{|e{kkUBTIkQ{fgvYWqtc?Q+CybrvEt|_g4A}_xfoN!`lwr z7plXb`U~18zAOYh(`hpFem(VaJ;%q5v~B;x+~ku3_0h?bayGW-)2W9h4p+=G)>G&2 zF6NZ+_^JF%o&SGiIr)wwfyo?AvEyR3^kqX+ZS9-+-NAKy=YWrF95y1-=6vH`Tm@rZ z(GRP7&R7ZDML!DN2^jU4=@V9Em-G3Yo*@;WFF70a=}}SP+*wNwV}*0uX=G@ID)VN4 zG76gzv%vAwiiXPAho-}y3b7P+`YywKwgXShqeOE2OHOdoN!`MUueZZVgvet|Q(4hk zbUZy^VJ)?Io{umKF+Mbp6-F878>^+qWYE0KQR&Ac{@GoAi!6QF5Ha#yA0Z<<&Ar)_ zB9ew8wkg|NKGEK~)L1>pJTPNvBuy!#T7jdMFZDny$4ZGN)c3;RG51%0&PQr|8 zF&+_mD`W{3M4N(Zx=1XEVTX%gh?tXgCUTR^M7u+i*5cQCO~S&{^W!)Q>ld}&Y6a7* z3jTbOu97KLVjuLQv9mR{vHEpgHv|rm3Os!5Y+>W$>_evRHi%HC(=Z5TSkNP52MooCLk{t73Kw3mlr6z|4zabkP6L&~X$5Dz*KFO_wY zxs3SurWnNA#IbX~r>&PWPE-$)Buq{|v^E765LfUkeo+d~nLprxrFy&tBRdg4+Ql<~ z4aspY>_M+P{3v3~zr!<9EXoF$gT2{lNHjbj2V>9RI%adJR)kRm^nRMlLxmA&l(I`w ziP_L-RD68Jh;Z*80JB`@E6Vw8vWCACghH17_zi;VB?1r3yCS20v82R+_4rR5wBw6Z z0C4u(kHO;n%V{mPM1(M(K1Vhn3! z+Sk1i@5ikNV&;#(s6jBrMCZ~01L>4m5DGNoU-{B2|H@~l@r|s*J?P~bh*P!XVVVlP z9&TU^>)Z``XN zua~kPd~vl}IhMnQTlGCGG3=NjS}FkALHNH$C3|$~TMJ9j;t zehnWbyD#?bTdXyFW^6CJ!`yXOZ_5l@BI#UoZ(_`J{>qNm#Xmp(`wa*@cUvS!{Qeu5 zUX{eeSPou#F=^H}+wjV~t}iTNw31x|E)GG|%4_&moetB%k7D}i z9~{kED*vzOB=SHkv_d}K(`2&XCB0~F;rJU)cy(sH;7T>Hatz&w*k<3deW#`x_7U>P zlCGBg)7Hd6jvQeYu2om?zAcZOGr-JC)s>n_BYI3JCbMZ*X&VfF3NdqKIvN+Na(TU0 zXFTADaZ*=V&f;8UGWtbFKP^`0C_MSa_5tU^k7M=Fe}b1ofMvhghX>lFu2BaJuZt<& zKAE_d%$+V)Cc8~5Prb*f)~p*j>6)rC59rnz^Y%sT?D|VR=AV0L9g}4neALd#u_~8% zaDmtLv?L{C6~Gyj8T-E!A@S++`ZTo+K7V_@6KR#bXXo+lHHtOTt%i}W*;9vk!L_EO z>BI*m6an#yk576YZmTUl4VD$)sS|=%S46o9tc9gnR1y1noIeTwe1CT2KLV_X)zU;k z-`RAt@W1)3;Pbn*jdTp;nqipfrWvurg#+9!qHPkG&zvKWnb=1vRsY`L2iin4(c@GD z`jYSm9n*&nZH3_xlV4Uaa*9349q_&$ygvJZAJG3sSd@=N{4Qj5@{5<% z;J!3dGSms!moa64I!e|ZepC=yAGiJmWFcFpLo&@I)zH&m*H0$!!VjyFQ?>*9XmMQWNV+BUei4g#-68)Nmh=Oxa3t2v*!qi|5c9XxRWc-tcTeOjwi&~7AE}gL@Vj1Iswk7gh&$ruwR*)6p zUOe5h zLv$mFP>3HZHd zcUr@dj+DJ_kU5wvBNy)()kE$m_aYEQ8Z@F-gm~fATV-=+3i$CCBDkoKu#nWIZ%sm) zT<41=e`1-9t+pwnJQN-U;Nww8^!1lrZ(4fsr9G00N!8eSH-kHF0)J0&6ITucKDEJ>n5KH{$5NO}<825XiEv))}>{Ije7j14zuXSQQgs7rxOvy6nc6B2qeBFl7 zCFfXA7#K0HJuKf&J4{U9&6Md&_NtmYbtN2Ki7B#^He0PFCltl>M#oHd`7o$>xrme& z$Nn!A3$6ag6g@U;5ePCNpZ;7iVDc3#|0Bh#zo}-fOE_fKDUcq5r6SR;6jg|&E?>D< zF|eyV6$pZcrNxJ4acL1S@Xx<$xJg-pUZaxT0&9KmTsZnd7VR{Y-5I&TBqOOHQ6y&g G;r{_zpKt*H literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/1_mode_selection.png b/Apps/MidiController/images/1_mode_selection.png new file mode 100644 index 0000000000000000000000000000000000000000..8802d1e42bb8e6107da79c3c4a6b6401183c4ddf GIT binary patch literal 53507 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 literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/3_beats_mode.png b/Apps/MidiController/images/3_beats_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..4728533bd4644dc09e839639116d22cee2895550 GIT binary patch literal 31370 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 literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/5_drop_mode.png b/Apps/MidiController/images/5_drop_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..31c9137d84616ff9cf1ec238524a701ef9e50a78 GIT binary patch literal 32245 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 literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/8_arp_mode.png b/Apps/MidiController/images/8_arp_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..731c295ae5d287dc0e3ea35efb93737a395ca76f GIT binary patch literal 43456 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 VM87cR|M#xEYga9dDh-`t{{shZFQ5Pb literal 0 HcmV?d00001 diff --git a/Apps/MidiController/images/9_grid_mode.png b/Apps/MidiController/images/9_grid_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..08ca042d70bfea1287c62a875102edf00811178c GIT binary patch literal 23991 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 yQJ(Aq*(=}oKPIA +#include + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// M5Stack 8Encoder (addr 0x41) + LCD Unit (addr 0x3E) +// Both are on PaHub (addr 0x70) on the grove port in I2C mode (Tab5 Port A, SDA=53 SCL=54). +// Looked up via findGroveI2cDevice() (GroveLookup.h) rather than a hardcoded device name: the +// grove port device itself ("grove0") is a mode-switchable I2C/UART router (GROVE_TYPE), not an +// I2C controller - passing it directly into i2c_controller_*() calls crashes (wrong driver data +// layout). The actual I2C-master child device it spawns in I2C mode is named "grove0_i2c", but +// even that name isn't hardcoded here since grove port naming isn't guaranteed. +// +// The app probes every software channel 0-5 at startup; channel numbers are +// 0-based and any port works. The current test setup uses CH2 for the encoder +// and CH3 for the LCD. +// +// Per-mode encoder → parameter mapping: +// EUCLID : knob 0 → sel.steps; knob 1 → sel.events; knob 2 → sel.rot; knob 3 → sel.note +// knob 4 → BPM; knobs 5-7 → steps for voices 6-8; button i → select voice i +// BABY8 : knob i (0-7) → note for step i; button i → toggle gate i +// DIMENSIONS : knob 0-3 → A/B/C/D; knob 4 → equation; knob 5 → BPM; knob 6 → oct; knob 7 → rnd +// button 0 → play/stop; button 1 → reset +// TB3PO : knob 0 → density; knob 1 → BPM; knob 2 → key; knob 3 → octave +// knob 4 → scale; knob 5 → regen-on-turn; button 0 → play/stop; button 1 → regen +// RAGA : knob 0 → raga; knob 1 → tala; knob 2 → root; knob 3 → BPM +// button 0 → play/stop; button 1 → drone toggle +// --------------------------------------------------------------------------- + +static constexpr auto* TAG = "EncoderInput"; +static constexpr int POLL_MS = 100; + +static UnitPaHub s_hub; +static Unit8Encoder s_enc; +static Device* s_i2cDev = nullptr; +// Discovered in encoderInit() by probing every PaHub channel (0-5); the 8Encoder can be plugged into any port on the hub. +static uint8_t s_encChannel = UnitPaHub::NO_CHANNEL; + +// --------------------------------------------------------------------------- +// LED colour tables +// --------------------------------------------------------------------------- + +// flushLeds() expects LED_COUNT (9) entries: 8 encoder LEDs + the switch LED. +// The app leaves the switch LED off, so the ninth entry is always 0. +static constexpr uint32_t EUCLID_COLORS[Unit8Encoder::LED_COUNT] = { + 0xFF4040, 0xFF9900, 0x44DD44, 0x44BBFF, + 0xFF44FF, 0xFFDD00, 0x00FFCC, 0xFF8888, + 0x000000 +}; +static constexpr uint32_t RAGA_COLORS[Unit8Encoder::LED_COUNT] = { + 0xFF8800, 0xFF6600, 0x00FFCC, 0xFFE000, + 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000 +}; +static constexpr uint32_t DIM_COLORS[Unit8Encoder::LED_COUNT] = { + 0x88DDFF, 0x88FFCC, 0xFFCC88, 0xFF88CC, + 0xFF8844, 0xFFE000, + 0xAAFF88, 0xFFAA44, + 0x000000 +}; + +// --------------------------------------------------------------------------- +// PaHub channel helpers +// --------------------------------------------------------------------------- + +static bool selectEncoder() { + return s_encChannel != UnitPaHub::NO_CHANNEL && s_hub.select(s_encChannel); +} + +// --------------------------------------------------------------------------- +// Per-mode dispatch (encoder side only - LED flush goes through Unit8Encoder) +// --------------------------------------------------------------------------- + +static void dispatchEuclid(MidiController* mc) { + auto& es = mc->euclid_; + int32_t deltas[8] = {}; + uint8_t buttons[8] = {}; + if (!selectEncoder()) return; + if (!s_enc.readAll(deltas, buttons)) return; + + bool changed = false; + auto& sv = es.voice[es.selectedVoice]; + + if (deltas[0]) { int s = sv.steps + deltas[0]; s = std::max(1,std::min(32,s)); sv.steps = s; if (sv.events > sv.steps) sv.events = sv.steps; changed = true; } + if (deltas[1]) { int e = sv.events + deltas[1]; e = std::max(0,std::min(sv.steps,e)); sv.events = e; changed = true; } + if (deltas[2]) { int safeSteps = std::max(1, sv.steps); int r = sv.rotation + deltas[2]; r = ((r % safeSteps) + safeSteps) % safeSteps; sv.rotation = r; changed = true; } + if (deltas[3]) { int n = sv.note + deltas[3]; n = std::max(0,std::min(127,n)); sv.note = n; changed = true; } + if (deltas[4]) { int b = es.bpm + deltas[4]; b = std::max(40,std::min(280,b)); es.bpm = b; if (es.bpmLbl) { char buf[8]; snprintf(buf,sizeof(buf),"%d",b); lv_label_set_text(es.bpmLbl,buf); } } + for (int i = 5; i <= 7; i++) { + if (deltas[i]) { auto& v = es.voice[i]; int s = v.steps + deltas[i]; s = std::max(1,std::min(32,s)); v.steps = s; if (v.events > v.steps) v.events = v.steps; changed = true; } + } + for (int i = 0; i < 8; i++) { + bool btn = (buttons[i] != 0); + if (btn && !mc->encoder_.btnPrev[i]) { es.selectedVoice = i; changed = true; } + mc->encoder_.btnPrev[i] = btn; + } + if (changed) euclidOnEncoderUpdate(mc); + + uint32_t pending[Unit8Encoder::LED_COUNT] = {}; + for (int i = 0; i < 8; i++) { + uint32_t c = EUCLID_COLORS[i]; + pending[i] = (i == es.selectedVoice) ? c : (c >> 2) & 0x3F3F3F; + } + selectEncoder(); + s_enc.flushLeds(pending); + lcdUpdate(mc); +} + +static void dispatchBaby8(MidiController* mc) { + auto& bs = mc->baby8_; + int32_t deltas[8] = {}; + uint8_t buttons[8] = {}; + if (!selectEncoder()) return; + if (!s_enc.readAll(deltas, buttons)) return; + + static const char* NOTE_NAMES[] = {"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"}; + bool stepChanged = false; + for (int i = 0; i < 8; i++) { + if (deltas[i]) { + int n = bs.steps[i].note + deltas[i]; + n = std::max(0, std::min(127, n)); + bs.steps[i].note = n; + if (bs.noteLbl[i]) { + char buf[8]; snprintf(buf,sizeof(buf),"%s%d",NOTE_NAMES[n%12],n/12-1); + lv_label_set_text(bs.noteLbl[i], buf); + } + stepChanged = true; + } + bool btn = (buttons[i] != 0); + if (btn && !mc->encoder_.btnPrev[i]) { + bs.steps[i].gate = !bs.steps[i].gate; + stepChanged = true; + } + mc->encoder_.btnPrev[i] = btn; + } + // Encoder edits must repaint the step buttons immediately; the timer only + // refreshes them while the sequencer is running. + if (stepChanged) baby8UpdateStepColors(mc); + + uint32_t pending[Unit8Encoder::LED_COUNT] = {}; + for (int i = 0; i < 8; i++) + pending[i] = (bs.playing && i == bs.currentStep) ? 0xFFFFFF + : (bs.steps[i].gate ? 0x004400 : 0x440000); + selectEncoder(); + s_enc.flushLeds(pending); + lcdUpdate(mc); +} + +static void dispatchDimensions(MidiController* mc) { + auto& ds = mc->dim_; + int32_t deltas[8] = {}; + uint8_t buttons[8] = {}; + if (!selectEncoder()) return; + if (!s_enc.readAll(deltas, buttons)) return; + + float* params[4] = {&ds.a,&ds.b,&ds.c,&ds.d}; + lv_obj_t** lbls[4] = {&ds.aLbl,&ds.bLbl,&ds.cLbl,&ds.dLbl}; + static const char* NAMES[4] = {"A","B","C","D"}; + for (int i = 0; i < 4; i++) { + if (deltas[i]) { + float v = *params[i] + deltas[i]; + v = std::max(0.0f, std::min(100.0f, v)); + *params[i] = v; + if (*lbls[i]) { char b[12]; snprintf(b,sizeof(b),"%s:%.0f",NAMES[i],v); lv_label_set_text(*lbls[i],b); } + } + } + if (deltas[4]) { int eq = ds.equation + deltas[4]; eq = std::max(1,std::min(20,eq)); ds.equation = eq; if (ds.eqLbl) { char b[12]; snprintf(b,sizeof(b),"Eq:%d",eq); lv_label_set_text(ds.eqLbl,b); } } + if (deltas[5]) { int bpm = ds.bpm + deltas[5]; bpm = std::max(40,std::min(280,bpm)); ds.bpm = bpm; if (ds.bpmLbl) { char b[8]; snprintf(b,sizeof(b),"%d",bpm); lv_label_set_text(ds.bpmLbl,b); } } + if (deltas[6]) dimChangeOctave(mc, deltas[6]); + if (deltas[7]) dimChangeRandom(mc, deltas[7]); + + for (int i = 0; i < 2; i++) { + bool btn = (buttons[i] != 0); + if (btn && !mc->encoder_.btnPrev[i]) { if (i==0) dimTogglePlay(mc); else dimReset(mc); } + mc->encoder_.btnPrev[i] = btn; + } + selectEncoder(); + s_enc.flushLeds(DIM_COLORS); + lcdUpdate(mc); +} + +static void dispatchTb3po(MidiController* mc) { + auto& ts = mc->tb3po_; + int32_t deltas[8] = {}; + uint8_t buttons[8] = {}; + if (!selectEncoder()) return; + if (!s_enc.readAll(deltas, buttons)) return; + + if (deltas[0]) { + int d = ts.density + deltas[0]; + d = std::max(0, std::min(16, d)); + if (d != ts.density) { + ts.density = d; + if (ts.densityLbl) { char b[8]; snprintf(b, sizeof(b), "D:%d", d); lv_label_set_text(ts.densityLbl, b); } + // Match the touch UI: a density change also draws a fresh seed and pattern. + tb3Regen(mc); + } + } + if (deltas[1]) { int bpm = ts.bpm + deltas[1]; bpm = std::max(40,std::min(280,bpm)); ts.bpm = bpm; if (ts.bpmLbl) { char b[8]; snprintf(b,sizeof(b),"%d",bpm); lv_label_set_text(ts.bpmLbl,b); } } + bool keyOrOctaveChanged = false; + if (deltas[2]) { ts.key = ((ts.key + deltas[2]) % 12 + 12) % 12; static const char* KS[] = {"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"}; if (ts.keyLbl) lv_label_set_text(ts.keyLbl, KS[ts.key]); keyOrOctaveChanged = true; } + if (deltas[3]) { int oct = ts.octave + deltas[3]; oct = std::max(1,std::min(6,oct)); ts.octave = oct; keyOrOctaveChanged = true; } + if (deltas[4]) tb3ChangeScale(mc, deltas[4]); + if (deltas[5]) tb3Regen(mc); + // The piano roll shows resolved note names; key/octave changes need a repaint + // even when the sequencer is stopped. + if (keyOrOctaveChanged) tb3Redraw(mc); + for (int i = 0; i < 2; i++) { + bool btn = (buttons[i] != 0); + if (btn && !mc->encoder_.btnPrev[i]) { if (i==0) tb3TogglePlay(mc); else tb3Regen(mc); } + mc->encoder_.btnPrev[i] = btn; + } + + uint32_t pending[Unit8Encoder::LED_COUNT] = {}; + for (int i = 0; i < 8; i++) + pending[i] = (ts.playing && i == (ts.currentStep / 4)) ? 0xFF6600 : 0x221100; + selectEncoder(); + s_enc.flushLeds(pending); + lcdUpdate(mc); +} + +static void dispatchRaga(MidiController* mc) { + auto& rs = mc->raga_; + int32_t deltas[8] = {}; + uint8_t buttons[8] = {}; + if (!selectEncoder()) return; + if (!s_enc.readAll(deltas, buttons)) return; + + static const char* RAGA_NAMES[] = {"Bhairavi","Lalit","Bhupali","Todi","Madhuvanti","Meghmalhar","Yaman","Malkauns"}; + static const char* TALA_NAMES[] = {"Teental","Rupak","Jhaptal"}; + + if (deltas[0]) { rs.raga = ((rs.raga + deltas[0]) % 8 + 8) % 8; if (rs.ragaLbl) lv_label_set_text(rs.ragaLbl, RAGA_NAMES[rs.raga]); if (rs.ragaBigLbl) lv_label_set_text(rs.ragaBigLbl, RAGA_NAMES[rs.raga]); } + if (deltas[1]) { rs.tala = ((rs.tala + deltas[1]) % 3 + 3) % 3; rs.talaBeat = 0; if (rs.talaLbl) lv_label_set_text(rs.talaLbl, TALA_NAMES[rs.tala]); if (rs.talaBigLbl) lv_label_set_text(rs.talaBigLbl, TALA_NAMES[rs.tala]); } + if (deltas[2]) { int r = rs.rootNote + deltas[2]; r = std::max(24,std::min(96,r)); rs.rootNote = r; if (rs.rootLbl) { static const char* N[] = {"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"}; char b[8]; snprintf(b,sizeof(b),"%s%d",N[r%12],r/12-1); lv_label_set_text(rs.rootLbl,b); } } + if (deltas[3]) { int bpm = rs.bpm + deltas[3]; bpm = std::max(40,std::min(240,bpm)); rs.bpm = bpm; if (rs.bpmLbl) { char b[8]; snprintf(b,sizeof(b),"%d",bpm); lv_label_set_text(rs.bpmLbl,b); } } + for (int i = 0; i < 2; i++) { + bool btn = (buttons[i] != 0); + if (btn && !mc->encoder_.btnPrev[i]) { if (i==0) ragaTogglePlay(mc); else ragaToggleDrone(mc); } + mc->encoder_.btnPrev[i] = btn; + } + selectEncoder(); + s_enc.flushLeds(RAGA_COLORS); + lcdUpdate(mc); +} + +// --------------------------------------------------------------------------- +// Poll timer +// --------------------------------------------------------------------------- + +static void cbEncoderPoll(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + // With no encoder, this timer still refreshes a detected LCD unit. + if (!mc->encoder_.present) { + lcdUpdate(mc); + return; + } + if (!s_hub.isPresent()) return; + + // After a mode switch, do one silent read to seed btnPrev with the live + // button state - prevents a physically-held button from registering as a + // fresh press in the new mode's dispatch. + if (mc->encoder_.btnSeedNext) { + mc->encoder_.btnSeedNext = false; + int32_t deltas[8] = {}; + uint8_t buttons[8] = {}; + if (selectEncoder()) { + s_enc.readAll(deltas, buttons); + for (int i = 0; i < 8; i++) + mc->encoder_.btnPrev[i] = (buttons[i] != 0); + } + return; + } + + switch (mc->currentMode_) { + case MidiMode::EUCLID: dispatchEuclid (mc); break; + case MidiMode::BABY8: dispatchBaby8 (mc); break; + case MidiMode::DIMENSIONS: dispatchDimensions(mc); break; + case MidiMode::TB3PO: dispatchTb3po (mc); break; + case MidiMode::RAGA: dispatchRaga (mc); break; + default: break; + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +void encoderSetLed(MidiController* mc, uint8_t idx, uint32_t rgb) { + if (!mc->encoder_.present || idx >= 8) return; + if (!s_hub.isPresent()) return; + selectEncoder(); + s_enc.setLed(idx, rgb); + // sync cache + mc->encoder_.ledColor[idx] = rgb; +} + +void encoderSetAllLeds(MidiController* mc, uint32_t rgb) { + if (!mc->encoder_.present) return; + if (!s_hub.isPresent()) return; + selectEncoder(); + s_enc.setAllLeds(rgb); + for (int i = 0; i < 8; i++) mc->encoder_.ledColor[i] = rgb; +} + +void encoderInit(MidiController* mc) { + if (mc->encoderTimer_) return; + + Device* dev = findGroveI2cDevice(); + if (!dev) { + ESP_LOGE(TAG, "grove0_i2c not ready"); + mc->encoder_.present = false; + return; + } + s_i2cDev = dev; + + // Init PaHub + if (!s_hub.begin(dev)) { + ESP_LOGW(TAG, "PaHub not found - no encoder or LCD"); + mc->encoder_.present = false; + s_i2cDev = nullptr; + return; + } + + // Find the 8Encoder by probing every PaHub channel - Unit8Encoder::begin() + // itself probes the I2C address and returns false if nothing answers, so trying each + // channel in turn and keeping the first hit is safe/cheap (6 channels, worst case). + s_encChannel = UnitPaHub::NO_CHANNEL; + for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS; ch++) { + if (!s_hub.select(ch)) continue; + if (s_enc.begin(dev)) { + s_encChannel = ch; + break; + } + } + if (s_encChannel == UnitPaHub::NO_CHANNEL) { + ESP_LOGW(TAG, "8Encoder not found on any PaHub channel"); + mc->encoder_.present = false; + // Still try LCD even without encoder + } else { + mc->encoder_.present = true; + memset(mc->encoder_.ledColor, 0, sizeof(mc->encoder_.ledColor)); + ESP_LOGI(TAG, "8Encoder ready on PaHub CH%d", (int)s_encChannel); + } + + // Find the LCD the same way (LcdDisplay.cpp's own channel scan). + lcdInit(&s_hub); + lcdSetMode(mc->currentMode_); + + if (mc->encoder_.present || lcdIsPresent()) { + mc->encoderTimer_ = lv_timer_create(cbEncoderPoll, POLL_MS, mc); + } +} + +void encoderStop(MidiController* mc) { + if (mc->encoderTimer_) { + lv_timer_delete(mc->encoderTimer_); + mc->encoderTimer_ = nullptr; + } + if (mc->encoder_.present && s_hub.isPresent()) { + selectEncoder(); + s_enc.setAllLeds(0x000000); + } + lcdStop(); + // Leave the multiplexer deselected so the next app does not accidentally + // find a unit through a stale channel and then lose it on the first switch. + if (s_hub.isPresent()) s_hub.deselect(); + memset(mc->encoder_.ledColor, 0, sizeof(mc->encoder_.ledColor)); + mc->encoder_.present = false; + s_i2cDev = nullptr; + s_encChannel = UnitPaHub::NO_CHANNEL; +} diff --git a/Apps/MidiController/main/Source/GroveLookup.cpp b/Apps/MidiController/main/Source/GroveLookup.cpp new file mode 100644 index 0000000..aac9850 --- /dev/null +++ b/Apps/MidiController/main/Source/GroveLookup.cpp @@ -0,0 +1,45 @@ +#include "GroveLookup.h" +#include +#include + +namespace { + +struct GroveChildSearch { + const DeviceType* childType; + Device* result; +}; + +bool onGroveDevice(Device* groveDevice, void* context) { + auto* search = static_cast(context); + + GroveMode mode; + if (grove_get_mode(groveDevice, &mode) != ERROR_NONE) return true; + if (mode != (search->childType == &I2C_CONTROLLER_TYPE ? GROVE_MODE_I2C : GROVE_MODE_UART)) return true; + + device_for_each_child(groveDevice, search, [](Device* child, void* ctx) -> bool { + auto* s = static_cast(ctx); + if (device_get_type(child) == s->childType && device_is_ready(child)) { + s->result = child; + return false; // stop - found it + } + return true; + }); + + return search->result == nullptr; // stop outer iteration once found +} + +Device* findGroveChild(const DeviceType* childType) { + GroveChildSearch search{childType, nullptr}; + device_for_each_of_type(&GROVE_TYPE, &search, onGroveDevice); + return search.result; +} + +} // namespace + +Device* findGroveI2cDevice() { + return findGroveChild(&I2C_CONTROLLER_TYPE); +} + +Device* findGroveUartDevice() { + return findGroveChild(&UART_CONTROLLER_TYPE); +} diff --git a/Apps/MidiController/main/Source/GroveLookup.h b/Apps/MidiController/main/Source/GroveLookup.h new file mode 100644 index 0000000..fd9cbb1 --- /dev/null +++ b/Apps/MidiController/main/Source/GroveLookup.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +// Finds the I2C controller device exposed by the first grove port currently +// in GROVE_MODE_I2C, by checking each grove port's child device type +// (not by name - grove port names are not guaranteed, e.g. "port_a"). +Device* findGroveI2cDevice(); + +// Same as findGroveI2cDevice() but for UART mode. +Device* findGroveUartDevice(); diff --git a/Apps/MidiController/main/Source/LcdDisplay.cpp b/Apps/MidiController/main/Source/LcdDisplay.cpp new file mode 100644 index 0000000..c4390fb --- /dev/null +++ b/Apps/MidiController/main/Source/LcdDisplay.cpp @@ -0,0 +1,814 @@ +#include "LcdDisplay.h" +#include "GroveLookup.h" + +#include +#include +#include +#include + +static constexpr auto* TAG = "LcdDisplay"; + +// --------------------------------------------------------------------------- +// Hardware +// --------------------------------------------------------------------------- + +static UnitPaHub* s_hub = nullptr; +static UnitLcd s_lcd; +static Device* s_i2cDev = nullptr; +// Discovered in lcdInit() by probing every PaHub channel (0-5); the LCD unit can be plugged into any port on the hub. +static uint8_t s_lcdChannel = UnitPaHub::NO_CHANNEL; + +// --------------------------------------------------------------------------- +// State cache - only redraw when something actually changed +// --------------------------------------------------------------------------- + +static MidiMode s_mode = MidiMode::HOME; +static bool s_dirty = true; // force full redraw on first update + +// Per-mode cached values (simple structs with sentinels so first draw always fires) +struct RagaCache { int raga=-1, tala=-1, root=-1, bpm=-1; bool playing=false, drone=false; }; +struct DimCache { int a=-1, b=-1, c=-1, d=-1, eq=-1, bpm=-1, octave=INT_MIN, rnd_cached=INT_MIN; bool playing=false; }; +struct Tb3Cache { int step=-1, key=-1, scale=-1, bpm=-1, density=-1; int patScale=-1, patDensity=-1; uint32_t seed=0; bool playing=false; }; +struct Baby8Cache { int note[8]={-1,-1,-1,-1,-1,-1,-1,-1}; bool gate[8]={}; int step=-1; bool playing=false; }; +struct EuclidCache{ int voice=-1, steps=-1, events=-1, rot=-1, note=-1, bpm=-1; bool playing=false; }; + +static RagaCache s_raga; +static DimCache s_dim; +static Tb3Cache s_tb3; +static Baby8Cache s_b8; +static EuclidCache s_euc; + +// --------------------------------------------------------------------------- +// Colours (RGB565) +// --------------------------------------------------------------------------- + +static constexpr uint16_t C_BG = 0x0000; // black +static constexpr uint16_t C_WHITE = 0xFFFF; +static constexpr uint16_t C_GRAY = 0x4A69; +static constexpr uint16_t C_DGRAY = 0x2104; +static constexpr uint16_t C_RED = 0xF800; +static constexpr uint16_t C_GREEN = 0x07E0; +static constexpr uint16_t C_BLUE = 0x001F; +static constexpr uint16_t C_ORANGE = 0xFD20; +static constexpr uint16_t C_YELLOW = 0xFFE0; +static constexpr uint16_t C_CYAN = 0x07FF; +static constexpr uint16_t C_MAGENTA = 0xF81F; +static constexpr uint16_t C_PURPLE = 0x801F; + +// Voice colours matching EUCLID_COLORS in EncoderInput +static constexpr uint16_t VOICE_COL[8] = { + 0xF800, 0xFD20, 0x47E0, 0x45FF, + 0xF81F, 0xFFE0, 0x07FE, 0xFC11 +}; + +// --------------------------------------------------------------------------- +// Drawing primitives +// --------------------------------------------------------------------------- + +static bool selectLcd() { + return s_hub && s_lcdChannel != UnitPaHub::NO_CHANNEL && s_hub->select(s_lcdChannel); +} + +static void fillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t col) { + if (w == 0 || h == 0) return; + s_lcd.fillRect(x, y, (uint8_t)(x + w - 1), (uint8_t)(y + h - 1), col); +} + +static void hline(uint8_t x, uint8_t y, uint8_t w, uint16_t col) { + fillRect(x, y, w, 1, col); +} + +// --------------------------------------------------------------------------- +// Minimal 5×7 bitmap font (printable ASCII 0x20-0x7E) +// Each character is 5 columns × 7 rows, packed into 5 bytes (LSB = top row). +// --------------------------------------------------------------------------- + +static const uint8_t FONT5x7[][5] = { + {0x00,0x00,0x00,0x00,0x00}, // ' ' + {0x00,0x00,0x5F,0x00,0x00}, // '!' + {0x00,0x07,0x00,0x07,0x00}, // '"' + {0x14,0x7F,0x14,0x7F,0x14}, // '#' + {0x24,0x2A,0x7F,0x2A,0x12}, // '$' + {0x23,0x13,0x08,0x64,0x62}, // '%' + {0x36,0x49,0x55,0x22,0x50}, // '&' + {0x00,0x05,0x03,0x00,0x00}, // ''' + {0x00,0x1C,0x22,0x41,0x00}, // '(' + {0x00,0x41,0x22,0x1C,0x00}, // ')' + {0x08,0x2A,0x1C,0x2A,0x08}, // '*' + {0x08,0x08,0x3E,0x08,0x08}, // '+' + {0x00,0x50,0x30,0x00,0x00}, // ',' + {0x08,0x08,0x08,0x08,0x08}, // '-' + {0x00,0x60,0x60,0x00,0x00}, // '.' + {0x20,0x10,0x08,0x04,0x02}, // '/' + {0x3E,0x51,0x49,0x45,0x3E}, // '0' + {0x00,0x42,0x7F,0x40,0x00}, // '1' + {0x42,0x61,0x51,0x49,0x46}, // '2' + {0x21,0x41,0x45,0x4B,0x31}, // '3' + {0x18,0x14,0x12,0x7F,0x10}, // '4' + {0x27,0x45,0x45,0x45,0x39}, // '5' + {0x3C,0x4A,0x49,0x49,0x30}, // '6' + {0x01,0x71,0x09,0x05,0x03}, // '7' + {0x36,0x49,0x49,0x49,0x36}, // '8' + {0x06,0x49,0x49,0x29,0x1E}, // '9' + {0x00,0x36,0x36,0x00,0x00}, // ':' + {0x00,0x56,0x36,0x00,0x00}, // ';' + {0x00,0x08,0x14,0x22,0x41}, // '<' + {0x14,0x14,0x14,0x14,0x14}, // '=' + {0x41,0x22,0x14,0x08,0x00}, // '>' + {0x02,0x01,0x51,0x09,0x06}, // '?' + {0x32,0x49,0x79,0x41,0x3E}, // '@' + {0x7E,0x11,0x11,0x11,0x7E}, // 'A' + {0x7F,0x49,0x49,0x49,0x36}, // 'B' + {0x3E,0x41,0x41,0x41,0x22}, // 'C' + {0x7F,0x41,0x41,0x22,0x1C}, // 'D' + {0x7F,0x49,0x49,0x49,0x41}, // 'E' + {0x7F,0x09,0x09,0x09,0x01}, // 'F' + {0x3E,0x41,0x49,0x49,0x7A}, // 'G' + {0x7F,0x08,0x08,0x08,0x7F}, // 'H' + {0x00,0x41,0x7F,0x41,0x00}, // 'I' + {0x20,0x40,0x41,0x3F,0x01}, // 'J' + {0x7F,0x08,0x14,0x22,0x41}, // 'K' + {0x7F,0x40,0x40,0x40,0x40}, // 'L' + {0x7F,0x02,0x04,0x02,0x7F}, // 'M' + {0x7F,0x04,0x08,0x10,0x7F}, // 'N' + {0x3E,0x41,0x41,0x41,0x3E}, // 'O' + {0x7F,0x09,0x09,0x09,0x06}, // 'P' + {0x3E,0x41,0x51,0x21,0x5E}, // 'Q' + {0x7F,0x09,0x19,0x29,0x46}, // 'R' + {0x46,0x49,0x49,0x49,0x31}, // 'S' + {0x01,0x01,0x7F,0x01,0x01}, // 'T' + {0x3F,0x40,0x40,0x40,0x3F}, // 'U' + {0x1F,0x20,0x40,0x20,0x1F}, // 'V' + {0x3F,0x40,0x38,0x40,0x3F}, // 'W' + {0x63,0x14,0x08,0x14,0x63}, // 'X' + {0x07,0x08,0x70,0x08,0x07}, // 'Y' + {0x61,0x51,0x49,0x45,0x43}, // 'Z' + {0x00,0x7F,0x41,0x41,0x00}, // '[' + {0x02,0x04,0x08,0x10,0x20}, // '\' + {0x00,0x41,0x41,0x7F,0x00}, // ']' + {0x04,0x02,0x01,0x02,0x04}, // '^' + {0x40,0x40,0x40,0x40,0x40}, // '_' + {0x00,0x01,0x02,0x04,0x00}, // '`' + {0x20,0x54,0x54,0x54,0x78}, // 'a' + {0x7F,0x48,0x44,0x44,0x38}, // 'b' + {0x38,0x44,0x44,0x44,0x20}, // 'c' + {0x38,0x44,0x44,0x48,0x7F}, // 'd' + {0x38,0x54,0x54,0x54,0x18}, // 'e' + {0x08,0x7E,0x09,0x01,0x02}, // 'f' + {0x08,0x14,0x54,0x54,0x3C}, // 'g' + {0x7F,0x08,0x04,0x04,0x78}, // 'h' + {0x00,0x44,0x7D,0x40,0x00}, // 'i' + {0x20,0x40,0x44,0x3D,0x00}, // 'j' + {0x7F,0x10,0x28,0x44,0x00}, // 'k' + {0x00,0x41,0x7F,0x40,0x00}, // 'l' + {0x7C,0x04,0x18,0x04,0x78}, // 'm' + {0x7C,0x08,0x04,0x04,0x78}, // 'n' + {0x38,0x44,0x44,0x44,0x38}, // 'o' + {0x7C,0x14,0x14,0x14,0x08}, // 'p' + {0x08,0x14,0x14,0x18,0x7C}, // 'q' + {0x7C,0x08,0x04,0x04,0x08}, // 'r' + {0x48,0x54,0x54,0x54,0x20}, // 's' + {0x04,0x3F,0x44,0x40,0x20}, // 't' + {0x3C,0x40,0x40,0x40,0x7C}, // 'u' + {0x1C,0x20,0x40,0x20,0x1C}, // 'v' + {0x3C,0x40,0x30,0x40,0x3C}, // 'w' + {0x44,0x28,0x10,0x28,0x44}, // 'x' + {0x0C,0x50,0x50,0x50,0x3C}, // 'y' + {0x44,0x64,0x54,0x4C,0x44}, // 'z' + {0x00,0x08,0x36,0x41,0x00}, // '{' + {0x00,0x00,0x7F,0x00,0x00}, // '|' + {0x00,0x41,0x36,0x08,0x00}, // '}' + {0x08,0x04,0x08,0x10,0x08}, // '~' +}; + +// Draw one character at pixel position (x,y), scale 1 or 2, colour/bg. +static void drawChar(uint8_t x, uint8_t y, char c, uint16_t col, uint16_t bg, uint8_t scale = 1) { + if (c < 0x20 || c > 0x7E) c = '?'; + const uint8_t* glyph = FONT5x7[c - 0x20]; + for (int col_i = 0; col_i < 5; col_i++) { + uint8_t col_data = glyph[col_i]; + for (int row_i = 0; row_i < 7; row_i++) { + uint16_t px_col = (col_data & (1 << row_i)) ? col : bg; + uint8_t px = (uint8_t)(x + col_i * scale); + uint8_t py = (uint8_t)(y + row_i * scale); + if (scale == 1) + s_lcd.drawPixel(px, py, px_col); + else + fillRect(px, py, scale, scale, px_col); + } + } +} + +// Draw a string. Returns x after last character. +static uint8_t drawStr(uint8_t x, uint8_t y, const char* str, uint16_t col, + uint16_t bg = C_BG, uint8_t scale = 1) { + uint8_t cw = (uint8_t)(6 * scale); // 5px + 1px gap + while (*str) { + drawChar(x, y, *str++, col, bg, scale); + x = (uint8_t)(x + cw); + } + return x; +} + +// Draw a string centred in a width-w region starting at x. +static void drawStrCentred(uint8_t x, uint8_t y, uint8_t w, const char* str, + uint16_t col, uint16_t bg = C_BG, uint8_t scale = 1) { + int len = (int)strlen(str); + int sw = len * 6 * scale - scale; // total pixel width (no trailing gap) + int offset = ((int)w - sw) / 2; + if (offset < 0) offset = 0; + drawStr((uint8_t)(x + offset), y, str, col, bg, scale); +} + +// Erase a text row (fill with background). +static void clearRow(uint8_t y, uint8_t h) { + fillRect(0, y, 135, h, C_BG); +} + +// --------------------------------------------------------------------------- +// Mode header (top 18px) +// --------------------------------------------------------------------------- + +static const char* modeName(MidiMode m) { + switch (m) { + case MidiMode::RAGA: return "RAGA"; + case MidiMode::DIMENSIONS: return "DIM"; + case MidiMode::TB3PO: return "TB-3PO"; + case MidiMode::BABY8: return "BABY8"; + case MidiMode::EUCLID: return "EUCLID"; + default: return "MIDI"; + } +} + +static uint16_t modeAccent(MidiMode m) { + switch (m) { + case MidiMode::RAGA: return C_ORANGE; + case MidiMode::DIMENSIONS: return C_CYAN; + case MidiMode::TB3PO: return C_GREEN; + case MidiMode::BABY8: return C_MAGENTA; + case MidiMode::EUCLID: return C_RED; + default: return C_GRAY; + } +} + +static void drawHeader(MidiMode m) { + uint16_t acc = modeAccent(m); + fillRect(0, 0, 135, 16, acc); + drawStrCentred(0, 4, 135, modeName(m), C_BG, acc, 2); + hline(0, 16, 135, C_DGRAY); +} + +// --------------------------------------------------------------------------- +// Note name helper +// --------------------------------------------------------------------------- + +static const char* NOTE_NAMES[] = { + "C","C#","D","D#","E","F","F#","G","G#","A","A#","B" +}; + +static void noteName(int midi, char* buf, int bufsz) { + int n = midi % 12; + int oct = midi / 12 - 1; + snprintf(buf, (size_t)bufsz, "%s%d", NOTE_NAMES[n], oct); +} + +// --------------------------------------------------------------------------- +// Per-mode draw functions +// --------------------------------------------------------------------------- + +// -- RAGA --------------------------------------------------------------- +// Layout (y=0..239, portrait): +// 0-16 header +// 18-65 raga name (large, 2×scale) +// 68-79 tala +// 82-93 root + BPM on same row +// 96- play/drone status bars + +static void drawRaga(const MidiController::RagaState& rs) { + static const char* RAGA_NAMES[] = { + "Bhairavi","Lalit","Bhupali","Todi", + "Madhuvanti","Meghmalhar","Yaman","Malkauns" + }; + static const char* TALA_NAMES[] = {"Teental","Rupak","Jhaptal"}; + + auto& c = s_raga; + bool fullRedraw = s_dirty; + + if (fullRedraw) { + fillRect(0, 0, 135, 240, C_BG); + drawHeader(MidiMode::RAGA); + // Static labels + drawStr(2, 70, "TALA", C_GRAY); + drawStr(2, 84, "ROOT", C_GRAY); + drawStr(70, 84, "BPM", C_GRAY); + hline(0, 98, 135, C_DGRAY); + } + + static constexpr int RAGA_NAMES_COUNT = (int)(sizeof(RAGA_NAMES) / sizeof(RAGA_NAMES[0])); + static constexpr int TALA_NAMES_COUNT = (int)(sizeof(TALA_NAMES) / sizeof(TALA_NAMES[0])); + + // Raga name (big, 2× scale) + if (fullRedraw || c.raga != rs.raga) { + clearRow(20, 46); + if (rs.raga >= 0 && rs.raga < RAGA_NAMES_COUNT) { + drawStrCentred(0, 22, 135, RAGA_NAMES[rs.raga], C_ORANGE, C_BG, 2); + } else { + drawStrCentred(0, 22, 135, "--", C_ORANGE, C_BG, 2); + } + c.raga = rs.raga; + } + + // Tala + if (fullRedraw || c.tala != rs.tala) { + fillRect(36, 68, 99, 10, C_BG); + if (rs.tala >= 0 && rs.tala < TALA_NAMES_COUNT) { + drawStr(36, 68, TALA_NAMES[rs.tala], C_YELLOW); + } else { + drawStr(36, 68, "--", C_YELLOW); + } + c.tala = rs.tala; + } + + // Root + if (fullRedraw || c.root != rs.rootNote) { + fillRect(36, 82, 32, 10, C_BG); + char buf[8]; noteName(rs.rootNote, buf, sizeof(buf)); + drawStr(36, 82, buf, C_CYAN); + c.root = rs.rootNote; + } + + // BPM + if (fullRedraw || c.bpm != rs.bpm) { + fillRect(96, 82, 39, 10, C_BG); + char buf[6]; snprintf(buf, sizeof(buf), "%d", rs.bpm); + drawStr(96, 82, buf, C_WHITE); + c.bpm = rs.bpm; + } + + // Play status bar + if (fullRedraw || c.playing != rs.playing) { + uint16_t col = rs.playing ? C_RED : C_DGRAY; + fillRect(0, 100, 135, 28, col); + drawStrCentred(0, 107, 135, rs.playing ? "PLAYING" : "STOPPED", C_WHITE, col, 2); + c.playing = rs.playing; + } + + // Drone bar + if (fullRedraw || c.drone != rs.droneOn) { + uint16_t col = rs.droneOn ? C_BLUE : C_DGRAY; + fillRect(0, 132, 135, 20, col); + drawStrCentred(0, 137, 135, rs.droneOn ? "DRONE ON" : "DRONE OFF", C_WHITE, col); + c.drone = rs.droneOn; + } + + // Knob legend at bottom + if (fullRedraw) { + uint8_t ly = 170; + struct { const char* k; const char* v; uint16_t col; } rows[] = { + {"K1","RAGA", C_ORANGE}, + {"K2","TALA", C_YELLOW}, + {"K3","ROOT", C_CYAN}, + {"K4","BPM", C_WHITE}, + {"B1","PLAY", C_RED}, + {"B2","DRON", C_BLUE}, + }; + for (auto& r : rows) { + fillRect(0, ly, 135, 10, C_BG); + drawStr(2, ly, r.k, C_GRAY); + drawStr(22, ly, r.v, r.col); + ly = (uint8_t)(ly + 12); + } + } +} + +// -- DIMENSIONS --------------------------------------------------------------- +// Layout: +// 0-16 header +// 18-77 A/B top row, C/D bottom row - coloured value bars +// 80-91 Eq + BPM +// 94-105 Oct + Rnd +// 108- play status + knob legend + +static void drawBar(uint8_t x, uint8_t y, uint8_t w, uint8_t h, + float val, float maxVal, uint16_t col) { + if (maxVal <= 0.0f) { fillRect(x, y, w, h, C_DGRAY); return; } + uint8_t filled = (uint8_t)((val / maxVal) * w); + if (filled > w) filled = w; + fillRect(x, y, filled, h, col); + if (filled < w) fillRect((uint8_t)(x + filled), y, (uint8_t)(w - filled), h, C_DGRAY); +} + +static void drawDimensions(const MidiController::DimensionsState& ds) { + auto& c = s_dim; + bool fullRedraw = s_dirty; + + if (fullRedraw) { + fillRect(0, 0, 135, 240, C_BG); + drawHeader(MidiMode::DIMENSIONS); + // Static labels + drawStr(2, 20, "A", UnitLcd::rgb888to565(0x88DDFF)); + drawStr(69, 20, "B", UnitLcd::rgb888to565(0x88FFCC)); + drawStr(2, 50, "C", UnitLcd::rgb888to565(0xFFCC88)); + drawStr(69, 50, "D", UnitLcd::rgb888to565(0xFF88CC)); + drawStr(2, 82, "EQ", C_ORANGE); + drawStr(70, 82, "BPM", C_YELLOW); + drawStr(2, 96, "OCT", UnitLcd::rgb888to565(0xAAFF88)); + drawStr(70, 96, "RND", UnitLcd::rgb888to565(0xFFAA44)); + hline(0, 110, 135, C_DGRAY); + } + + static const uint16_t BC[4] = { + UnitLcd::rgb888to565(0x88DDFF), UnitLcd::rgb888to565(0x88FFCC), + UnitLcd::rgb888to565(0xFFCC88), UnitLcd::rgb888to565(0xFF88CC) + }; + float vals[4] = { ds.a, ds.b, ds.c, ds.d }; + int cvals[4]= { (int)ds.a, (int)ds.b, (int)ds.c, (int)ds.d }; + int* cprev[4]= { &c.a, &c.b, &c.c, &c.d }; + + for (int i = 0; i < 4; i++) { + if (fullRedraw || *cprev[i] != cvals[i]) { + uint8_t bx = (i & 1) ? 67 : 2; + uint8_t by = (i < 2) ? 30 : 60; + // value text + fillRect(bx, by - 10, 60, 10, C_BG); + char buf[8]; snprintf(buf, sizeof(buf), "%.0f", vals[i]); + drawStr((uint8_t)(bx + 12), (uint8_t)(by - 10), buf, BC[i]); + // bar + drawBar(bx, by, 60, 8, vals[i], 100.0f, BC[i]); + *cprev[i] = cvals[i]; + } + } + + if (fullRedraw || c.eq != ds.equation) { + fillRect(20, 82, 46, 10, C_BG); + char buf[8]; snprintf(buf, sizeof(buf), "%d", ds.equation); + drawStr(20, 82, buf, C_ORANGE); + c.eq = ds.equation; + } + if (fullRedraw || c.bpm != ds.bpm) { + fillRect(90, 82, 45, 10, C_BG); + char buf[6]; snprintf(buf, sizeof(buf), "%d", ds.bpm); + drawStr(90, 82, buf, C_YELLOW); + c.bpm = ds.bpm; + } + if (fullRedraw || c.octave != ds.octave) { + fillRect(22, 96, 46, 10, C_BG); + char buf[8]; snprintf(buf, sizeof(buf), "%+d", ds.octave); + drawStr(22, 96, buf, UnitLcd::rgb888to565(0xAAFF88)); + c.octave = ds.octave; + } + int rndRounded = (int)ds.rnd; + if (fullRedraw || c.rnd_cached != rndRounded) { + fillRect(90, 96, 45, 10, C_BG); + char buf[8]; snprintf(buf, sizeof(buf), "%.0f", ds.rnd); + drawStr(90, 96, buf, UnitLcd::rgb888to565(0xFFAA44)); + c.rnd_cached = rndRounded; + } + + if (fullRedraw || c.playing != ds.playing) { + uint16_t col = ds.playing ? C_GREEN : C_DGRAY; + fillRect(0, 112, 135, 20, col); + drawStrCentred(0, 117, 135, ds.playing ? "PLAYING" : "STOPPED", C_WHITE, col); + c.playing = ds.playing; + } +} + +// -- TB3PO --------------------------------------------------------------- +// Layout: +// 0-16 header +// 18-49 32-step mini piano roll (6×12 px cells at an 8 px pitch, 2 rows of 16) +// 52-63 key + scale +// 66-77 density + BPM +// 80-91 play status + +static constexpr const char* TB3_SCALE_NAMES[] = { + "Major","Minor","Dorian","Penta","Blues","Chrome" +}; +static constexpr int TB3_SCALE_NAMES_COUNT = (int)(sizeof(TB3_SCALE_NAMES) / sizeof(TB3_SCALE_NAMES[0])); +static void drawTb3po(const MidiController::Tb3poState& ts) { + auto& c = s_tb3; + bool fullRedraw = s_dirty; + + if (fullRedraw) { + fillRect(0, 0, 135, 240, C_BG); + drawHeader(MidiMode::TB3PO); + drawStr(2, 54, "KEY", C_GREEN); + drawStr(70, 54, "SCALE", C_CYAN); + drawStr(2, 68, "DEN", C_YELLOW); + drawStr(70, 68, "BPM", C_WHITE); + hline(0, 82, 135, C_DGRAY); + } + + // 32-step piano roll - two rows of 16 cells (6×12 px, 2 px gaps) + // Row 0: steps 0-15, Row 1: steps 16-31 + // The pattern itself changes on regen, density, or scale edits even when the + // playback position does not, so those repaint all cells. A normal step + // advance only needs the old and new cursor cells repainted. + auto drawCell = [&](int i) { + int row = i / 16; + int col_i = i % 16; + uint8_t x = (uint8_t)(2 + col_i * 8); + uint8_t y = (uint8_t)(20 + row * 15); + bool active = ts.playing && (i == ts.currentStep); + bool hasNote = ts.steps[i].gate; + uint16_t col; + if (active) col = C_WHITE; + else if (hasNote) col = C_GREEN; + else col = C_DGRAY; + fillRect(x, y, 6, 12, col); + // Accent dot + if (ts.steps[i].accent && !active) + fillRect((uint8_t)(x+2), (uint8_t)(y+1), 2, 2, C_YELLOW); + }; + + bool patternChanged = fullRedraw || c.seed != ts.seed || + c.playing != ts.playing || c.patDensity != ts.density || c.patScale != ts.scale; + if (patternChanged) { + for (int i = 0; i < 32; i++) drawCell(i); + c.seed = ts.seed; + c.patDensity = ts.density; + c.patScale = ts.scale; + } else if (c.step != ts.currentStep) { + if (c.step >= 0 && c.step < 32) drawCell(c.step); + if (ts.currentStep >= 0 && ts.currentStep < 32 && ts.currentStep != c.step) + drawCell(ts.currentStep); + } + c.step = ts.currentStep; + + if (fullRedraw || c.key != ts.key) { + fillRect(22, 54, 46, 10, C_BG); + int safeKey = ((ts.key % 12) + 12) % 12; + drawStr(22, 54, KEY_NAMES[safeKey], C_GREEN); + c.key = ts.key; + } + if (fullRedraw || c.scale != ts.scale) { + fillRect(96, 54, 39, 10, C_BG); + drawStr(96, 54, TB3_SCALE_NAMES[ts.scale % TB3_SCALE_NAMES_COUNT], C_CYAN); + c.scale = ts.scale; + } + if (fullRedraw || c.density != ts.density) { + fillRect(22, 68, 46, 10, C_BG); + char buf[4]; snprintf(buf, sizeof(buf), "%d", ts.density); + drawStr(22, 68, buf, C_YELLOW); + c.density = ts.density; + } + if (fullRedraw || c.bpm != ts.bpm) { + fillRect(96, 68, 39, 10, C_BG); + char buf[6]; snprintf(buf, sizeof(buf), "%d", ts.bpm); + drawStr(96, 68, buf, C_WHITE); + c.bpm = ts.bpm; + } + if (fullRedraw || c.playing != ts.playing) { + uint16_t col = ts.playing ? C_GREEN : C_DGRAY; + fillRect(0, 84, 135, 20, col); + drawStrCentred(0, 89, 135, ts.playing ? "PLAYING" : "STOPPED", C_WHITE, col); + c.playing = ts.playing; + } +} + +// -- BABY8 --------------------------------------------------------------- +// Layout: +// 0-16 header +// 18- 8 step cells, each 28px tall: +// note name, gate indicator, current step highlight + +static void drawBaby8(const MidiController::Baby8State& bs) { + auto& c = s_b8; + bool fullRedraw = s_dirty; + + if (fullRedraw) { + fillRect(0, 0, 135, 240, C_BG); + drawHeader(MidiMode::BABY8); + } + + for (int i = 0; i < 8; i++) { + // The cursor moved: only the cells losing/gaining the highlight need a + // repaint. A play/stop transition only affects the current cell too. + bool highlightChanged = fullRedraw || c.playing != bs.playing || + (c.step != bs.currentStep && (i == c.step || i == bs.currentStep)); + bool noteChanged = fullRedraw || c.note[i] != bs.steps[i].note; + bool gateChanged = fullRedraw || c.gate[i] != bs.steps[i].gate; + + if (!highlightChanged && !noteChanged && !gateChanged) continue; + + uint8_t y = (uint8_t)(18 + i * 27); + bool isCurrent = bs.playing && (i == bs.currentStep); + bool gateOn = bs.steps[i].gate; + + uint16_t bgcol = isCurrent ? C_WHITE : (gateOn ? C_DGRAY : C_BG); + uint16_t fgcol = isCurrent ? C_BG : (gateOn ? C_GREEN : C_GRAY); + + fillRect(0, y, 135, 25, bgcol); + + // Step number + char sbuf[4]; snprintf(sbuf, sizeof(sbuf), "%d", i + 1); + drawStr(4, (uint8_t)(y + 8), sbuf, isCurrent ? C_BG : C_GRAY, bgcol); + + // Note name + char nbuf[8]; noteName(bs.steps[i].note, nbuf, sizeof(nbuf)); + drawStr(24, (uint8_t)(y + 4), nbuf, fgcol, bgcol, 2); + + // Gate dot + uint16_t dotcol = gateOn ? (isCurrent ? C_BG : C_GREEN) : C_DGRAY; + fillRect(115, (uint8_t)(y + 8), 12, 8, dotcol); + + hline(0, (uint8_t)(y + 25), 135, C_DGRAY); + + c.note[i] = bs.steps[i].note; + c.gate[i] = bs.steps[i].gate; + } + c.step = bs.currentStep; + c.playing = bs.playing; +} + +// -- EUCLID --------------------------------------------------------------- +// Layout: +// 0-16 header +// 18-33 voice tabs (8 mini boxes) +// 38-53 selected voice name (2x scale) +// 80-150 steps / events / rotation / note / BPM +// 156-174 play status, 176-236 knob legend + +static void drawEuclid(const MidiController::EuclidState& es) { + auto& c = s_euc; + bool fullRedraw = s_dirty; + + if (fullRedraw) { + fillRect(0, 0, 135, 240, C_BG); + drawHeader(MidiMode::EUCLID); + drawStr(2, 80, "STEPS", C_GRAY); + drawStr(2, 94, "EVENTS", C_GRAY); + drawStr(2, 108, "ROT", C_GRAY); + drawStr(2, 122, "NOTE", C_GRAY); + drawStr(2, 140, "BPM", C_GRAY); + hline(0, 154, 135, C_DGRAY); + } + + // Capture voice-changed flag before updating the cache + bool voiceChanged = (c.voice != es.selectedVoice); + + // Selected voice index + int idx = es.selectedVoice & 7; + + // Voice tabs + if (fullRedraw || voiceChanged) { + for (int i = 0; i < 8; i++) { + uint8_t tx = (uint8_t)(i * 17); + uint16_t col = VOICE_COL[i]; + uint16_t bg = (i == idx) ? col : C_DGRAY; + fillRect(tx, 18, 15, 15, bg); + char vb[2] = { (char)('1' + i), 0 }; + drawChar((uint8_t)(tx + 4), 22, vb[0], + (i == idx) ? C_BG : C_GRAY, bg); + } + c.voice = es.selectedVoice; + } + + // Selected voice name + if (fullRedraw || voiceChanged) { + clearRow(36, 18); + char vbuf[16]; snprintf(vbuf, sizeof(vbuf), "VOICE %d", idx + 1); + drawStrCentred(0, 38, 135, vbuf, VOICE_COL[idx], C_BG, 2); + } + + const auto& sv = es.voice[idx]; + + auto drawVal = [&](uint8_t y, int val, int& cached, uint16_t col) { + if (fullRedraw || cached != val || voiceChanged) { + fillRect(54, y, 81, 10, C_BG); + char b[8]; snprintf(b, sizeof(b), "%d", val); + drawStr(54, y, b, col); + cached = val; + } + }; + + drawVal(80, sv.steps, c.steps, C_YELLOW); + drawVal(94, sv.events, c.events, C_CYAN); + drawVal(108, sv.rotation, c.rot, UnitLcd::rgb888to565(0xAAFF88)); + + if (fullRedraw || c.note != sv.note || voiceChanged) { + fillRect(54, 122, 81, 10, C_BG); + char nb[8]; noteName(sv.note, nb, sizeof(nb)); + drawStr(54, 122, nb, UnitLcd::rgb888to565(0xFF88CC)); + c.note = sv.note; + } + + if (fullRedraw || c.bpm != es.bpm) { + fillRect(54, 140, 81, 10, C_BG); + char b[8]; snprintf(b, sizeof(b), "%d", es.bpm); + drawStr(54, 140, b, C_WHITE); + c.bpm = es.bpm; + } + + if (fullRedraw || c.playing != es.playing) { + uint16_t col = es.playing ? C_RED : C_DGRAY; + fillRect(0, 156, 135, 18, col); + drawStrCentred(0, 160, 135, es.playing ? "PLAYING" : "STOPPED", C_WHITE, col); + c.playing = es.playing; + } + + // Knob legend + if (fullRedraw || voiceChanged) { + uint8_t ly = 176; + struct { const char* k; const char* v; uint16_t col; } rows[] = { + {"K1","STEPS", C_YELLOW}, + {"K2","EVENTS", C_CYAN}, + {"K3","ROT", UnitLcd::rgb888to565(0xAAFF88)}, + {"K4","NOTE", UnitLcd::rgb888to565(0xFF88CC)}, + {"K5","BPM", C_WHITE}, + {"B", "VOICE", VOICE_COL[idx]}, + }; + for (auto& r : rows) { + fillRect(0, ly, 135, 10, C_BG); + drawStr(2, ly, r.k, C_GRAY); + drawStr(22, ly, r.v, r.col); + ly = (uint8_t)(ly + 10); + } + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +bool lcdInit(UnitPaHub* hub) { + s_hub = hub; + if (!s_hub) return false; + + // findGroveI2cDevice() (GroveLookup.h), not a hardcoded "i2c1" name - the grove port device + // is a mode-switchable I2C/UART router, not an I2C controller itself, so device_get_by_name + // on the router's own name crashes (wrong driver data layout for i2c_controller_*() calls). + Device* i2cDev = findGroveI2cDevice(); + if (!i2cDev) { + ESP_LOGE(TAG, "grove0_i2c not found"); + return false; + } + s_i2cDev = i2cDev; + + // Find the LCD by probing every PaHub channel - UnitLcd::begin() + // itself probes the I2C address and returns false if nothing answers, so trying each channel + // in turn and keeping the first hit is safe/cheap (6 channels, worst case). + s_lcdChannel = UnitPaHub::NO_CHANNEL; + for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS; ch++) { + if (!s_hub->select(ch)) continue; + if (s_lcd.begin(i2cDev, UnitLcd::DEFAULT_ADDR)) { + s_lcdChannel = ch; + break; + } + } + if (s_lcdChannel == UnitPaHub::NO_CHANNEL) { + ESP_LOGW(TAG, "LCD unit not found on any PaHub channel"); + s_i2cDev = nullptr; + return false; + } + s_lcd.setRotation(0); // portrait 135×240 + s_lcd.setBrightness(180); + s_lcd.fillScreen(C_BG); + ESP_LOGI(TAG, "LCD ready on PaHub CH%d", (int)s_lcdChannel); + return true; +} + +void lcdStop() { + if (s_lcd.isPresent() && selectLcd()) { + s_lcd.setBrightness(0); + s_lcd.fillScreen(C_BG); + } + s_i2cDev = nullptr; + s_lcdChannel = UnitPaHub::NO_CHANNEL; +} + +bool lcdIsPresent() { + return s_lcd.isPresent(); +} + +void lcdSetMode(MidiMode mode) { + s_mode = mode; + s_dirty = true; + // Reset all caches so next lcdUpdate does a full redraw + s_raga = {}; s_raga.raga = -1; + s_dim = {}; s_dim.a = -1; + s_tb3 = {}; s_tb3.step = -1; + s_b8 = {}; for (auto& n : s_b8.note) n = -1; + s_euc = {}; s_euc.voice = -1; +} + +void lcdUpdate(MidiController* mc) { + if (!s_lcd.isPresent() || !s_hub) return; + if (!selectLcd()) return; + + switch (s_mode) { + case MidiMode::RAGA: drawRaga (mc->raga_); break; + case MidiMode::DIMENSIONS: drawDimensions(mc->dim_); break; + case MidiMode::TB3PO: drawTb3po (mc->tb3po_); break; + case MidiMode::BABY8: drawBaby8 (mc->baby8_); break; + case MidiMode::EUCLID: drawEuclid (mc->euclid_);break; + default: { + if (s_dirty) { + fillRect(0, 0, 135, 240, C_BG); + drawHeader(s_mode); + } + break; + } + } + s_dirty = false; +} diff --git a/Apps/MidiController/main/Source/LcdDisplay.h b/Apps/MidiController/main/Source/LcdDisplay.h new file mode 100644 index 0000000..bccfe63 --- /dev/null +++ b/Apps/MidiController/main/Source/LcdDisplay.h @@ -0,0 +1,49 @@ +/* + * LcdDisplay.h - per-mode LCD content for the M5Stack LCD Unit (135×240) + * + * The LCD unit usually sits on PaHub CH5, but lcdInit() probes every channel. + * All public functions select the discovered channel, draw, then leave it + * selected. EncoderInput re-selects the encoder channel before it reads, so + * there is no conflict as long as they don't run concurrently - both are + * called from the same LVGL timer thread. + * + * Layout per mode (portrait 135×240, white text on dark background): + * + * HOME / idle: + * Full-screen mode name + "MIDI CTRL" branding + * + * RAGA: + * Large raga name, tala, root note, BPM, play/drone status + * + * DIMENSIONS: + * A B C D values as a 2×2 grid with colour bars, equation, BPM + * + * TB3PO: + * 32-step piano-roll mini view (current step highlighted), key, scale, BPM + * + * BABY8: + * 8-step note grid - note name per step, current step highlighted, gates + * + * EUCLID: + * Selected voice name + params (steps/events/rotation/note), BPM + */ +#pragma once + +#include +#include +#include "MidiController.h" + +// Initialise LCD subsystem. Pass the already-initialised PaHub. +// Returns false if the LCD unit is not found on any PaHub channel. +bool lcdInit(UnitPaHub* hub); + +// Release resources (called from encoderStop). +void lcdStop(); + +bool lcdIsPresent(); + +// Called from switchToMode - clears screen and draws static mode chrome. +void lcdSetMode(MidiMode mode); + +// Called from each dispatch function after state changes. +void lcdUpdate(MidiController* mc); diff --git a/Apps/MidiController/main/Source/MidiController.cpp b/Apps/MidiController/main/Source/MidiController.cpp new file mode 100644 index 0000000..449fcd1 --- /dev/null +++ b/Apps/MidiController/main/Source/MidiController.cpp @@ -0,0 +1,1054 @@ +#include "MidiController.h" +#include "LcdDisplay.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr auto* TAG = "MidiController"; + +// App-scoped settings: only which transport was chosen, never whether it was running. +static constexpr auto* APP_ID = "tactility.midicontroller"; +static constexpr auto* SETTINGS_FILE = "settings.properties"; +static constexpr auto* PREF_TRANSPORT = "transport"; // 0 = BLE, 1 = USB +// BLE advertised name. It is also the default BLE identity, whose hash selects the address and +// bond slot, so it must stay stable across releases or hosts have to pair again. +static constexpr auto* BLE_DEVICE_NAME = "Tactility MIDI"; + +MidiController* MidiController::s_instance = nullptr; + +// --------------------------------------------------------------------------- +// Theme colors +// --------------------------------------------------------------------------- + +static lv_color_t MC_WARNING() { return lv_color_hex(0xFFE000); } +static lv_color_t MC_ERROR() { return lv_color_hex(0xFF2020); } + +// MIDI note numbers for the four drum tracks (KICK, SNARE, HIHAT, OPEN) +static constexpr uint8_t DRUM_NOTES[] = { 36, 38, 42, 46 }; + +// Per-track colors for BEATS mode (KICK, SNARE, HIHAT, OPEN) +static lv_color_t TRACK_COLOR(int t) { + switch (t) { + case 0: return MC_ERROR(); + case 1: return MC_WARNING(); + case 2: return MC_PRIMARY(); + case 3: return lv_color_hex(0xCC44FF); + default: return MC_SURFACE(); + } +} + +// --------------------------------------------------------------------------- +// Scaling helper - always based on display width, never parent +// --------------------------------------------------------------------------- + + +static inline lv_coord_t dispW() { + return (lv_coord_t)lv_display_get_horizontal_resolution(nullptr); +} + +static inline lv_coord_t dispH() { + return (lv_coord_t)lv_display_get_vertical_resolution(nullptr); +} + +// --------------------------------------------------------------------------- +// Style helpers +// --------------------------------------------------------------------------- + +static void styleContainer(lv_obj_t* obj, lv_color_t bg = {}, bool transparent = true) { + if (transparent) { + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + } else { + lv_obj_set_style_bg_color(obj, bg, 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0); + } + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + lv_obj_set_scrollable(obj, false); +} + +// --------------------------------------------------------------------------- +// MIDI transport - BLE (MediaKeys-style radio management) or USB, chosen from the transport +// selector overlay before the toolbar switch is turned on. sendMidi() dispatches to whichever +// one is currently running (only one of bleRunning_/usbRunning_ is ever true). +// --------------------------------------------------------------------------- + +// Only reached via handleTransportToggle() (radio already on) or handleBtEvent() (radio just +// turned on after handleTransportToggle() requested it) - btDevice_ is always the ref'd pointer +// device_get_first_by_type() gave handleTransportToggle() in both cases, so every path that +// clears it here must device_put() first. +void MidiController::startBleMidi() { + radioEnabling_ = false; + struct Device* midiDev = bluetooth_midi_get(); + if (!midiDev) { + ESP_LOGE(TAG, "No BLE MIDI device after radio on"); + if (btDevice_) { + unsubscribeBtEvents(); + device_put(btDevice_); + } + btDevice_ = nullptr; + bleRunning_ = false; + failTransportStart(); + return; + } + error_t startErr = bluetooth_midi_start(midiDev); + device_put(midiDev); // bluetooth_midi_get() returns a referenced device + if (startErr != ERROR_NONE) { + ESP_LOGW(TAG, "bluetooth_midi_start failed"); + if (btDevice_) { + unsubscribeBtEvents(); + device_put(btDevice_); + } + btDevice_ = nullptr; + bleRunning_ = false; + failTransportStart(); + return; + } + bleRunning_ = true; + ESP_LOGI(TAG, "BLE MIDI started"); +} + +void MidiController::stopBleMidi() { + if (bleRunning_) { + sendMidi(0xB0, 123, 0); // CC 123 = All Notes Off + struct Device* midiDev = bluetooth_midi_get(); + if (midiDev) { + bluetooth_midi_stop(midiDev); + device_put(midiDev); // bluetooth_midi_get() returns a referenced device + } + bleRunning_ = false; + } +} + +void MidiController::startUsbMidi() { + struct Device* dev = usb_midi_device_get(); + if (!dev) { + ESP_LOGE(TAG, "No USB MIDI device available"); + failTransportStart(); + return; + } + usb_midi_device_set_name(dev, "Tactility MIDI"); + error_t err = usb_midi_device_start(dev); + device_put(dev); + if (err != ERROR_NONE) { + ESP_LOGW(TAG, "usb_midi_device_start failed: %d (slot busy?)", (int)err); + failTransportStart(); + return; + } + usbRunning_ = true; + ESP_LOGI(TAG, "USB MIDI started"); +} + +void MidiController::stopUsbMidi() { + if (usbRunning_) { + sendMidi(0xB0, 123, 0); // CC 123 = All Notes Off + struct Device* dev = usb_midi_device_get(); + if (dev) { + usb_midi_device_stop(dev); + device_put(dev); + } + usbRunning_ = false; + } +} + +void MidiController::handleBtEvent(const struct BtEvent& event) { + if (event.type == BT_EVENT_RADIO_STATE_CHANGED) { + ESP_LOGI(TAG, "BT radio state: %d", (int)event.radio_state); + if (event.radio_state == BT_RADIO_STATE_ON && radioEnabling_) { + startBleMidi(); + } else if (event.radio_state == BT_RADIO_STATE_OFF && bleRunning_) { + ESP_LOGI(TAG, "BT radio turned off externally, stopping MIDI"); + sendMidi(0xB0, 123, 0); // best-effort All Notes Off; the radio may already be down + bleRunning_ = false; + failTransportStart(); + } + } +} + +void MidiController::pollBtEvents() { + if (!btEventSubscribed_) return; + struct BtEvent event {}; + while (bluetooth_event_poll(&btEventSub_, &event) == ERROR_NONE) { + handleBtEvent(event); + } + // If the radio dropped externally, the event handler only updated the UI. + // Do not unsubscribe from inside the poll callback (that would destroy the + // subscription while the loop is still using it); clean up once the queue + // is drained and no enable request is in flight. + if (!bleRunning_ && !radioEnabling_ && btEventSubscribed_) { + unsubscribeBtEvents(); + if (btDevice_) { + device_put(btDevice_); + btDevice_ = nullptr; + } + radioWasOff_ = false; + } +} + +void MidiController::subscribeBtEvents() { + if (btEventSubscribed_ || btDevice_ == nullptr) return; + btEventSub_ = {}; + bluetooth_event_subscribe(btDevice_, &btEventSub_, eventGroup_); + btEventSubscribed_ = true; +} + +void MidiController::unsubscribeBtEvents() { + if (!btEventSubscribed_) return; + if (btDevice_ != nullptr) { + bluetooth_event_unsubscribe(btDevice_, &btEventSub_); + } + btEventSubscribed_ = false; +} + +void MidiController::failTransportStart() { + lvgl_lock(); + if (switchWidget_) lv_obj_remove_state(switchWidget_, LV_STATE_CHECKED); + if (transportBtn_) lv_obj_remove_state(transportBtn_, LV_STATE_DISABLED); + lvgl_unlock(); +} + +void MidiController::enableTransportButton() { + lvgl_lock(); + if (transportBtn_) lv_obj_remove_state(transportBtn_, LV_STATE_DISABLED); + lvgl_unlock(); +} + +// Runs on the LVGL task: only the UI changes here. Starting a transport can write to flash or NVS, +// which isn't possible on the LVGL task, so the app's task does that in applyTransport(). +void MidiController::handleTransportToggle(bool enabled) { + ESP_LOGI(TAG, "Transport toggle: %s", enabled ? "ON" : "OFF"); + if (enabled) { + // Close the transport chooser if it is still open + if (transportOverlay_) { + lv_obj_del(transportOverlay_); + transportOverlay_ = nullptr; + // First launch: the chooser was the only content. Open HOME before starting, + // otherwise the switch would leave the content area empty. + if (currentViewRoot_ == nullptr) { + currentMode_ = MidiMode::HOME; + buildHomeView(contentArea_); + } + } + if (transportBtn_) lv_obj_add_state(transportBtn_, LV_STATE_DISABLED); + } + transportRequest_.store(enabled ? 1 : 0); + task_event_group_signal(eventGroup_, transportRequestBit_); +} + +// Runs on the app's task +void MidiController::applyTransport(bool enabled) { + radioEnabling_ = false; + if (enabled) { + if (activeTransport_ == MidiTransport::USB) { + startUsbMidi(); + return; + } + + // bluetooth_find_first_ready_device() only finds an already-started device, so it can't + // be used here: on a cold boot the BT device may not be started yet so this toggle would silently do nothing. + // device_get_first_by_type() finds the device regardless of ready state; start it on demand. + if (device_get_first_by_type(&BLUETOOTH_TYPE, &btDevice_) != ERROR_NONE) { + ESP_LOGE(TAG, "No Bluetooth device found"); + failTransportStart(); + return; + } + if (!device_is_ready(btDevice_) && device_start(btDevice_) != ERROR_NONE) { + ESP_LOGE(TAG, "Failed to start Bluetooth device"); + device_put(btDevice_); + btDevice_ = nullptr; + failTransportStart(); + return; + } + bluetooth_set_device_name(btDevice_, BLE_DEVICE_NAME); + subscribeBtEvents(); + + enum BtRadioState radioState; + bluetooth_get_radio_state(btDevice_, &radioState); + if (radioState == BT_RADIO_STATE_ON) { + radioWasOff_ = false; + startBleMidi(); + } else { + ESP_LOGI(TAG, "BT radio not on (state=%d), enabling...", (int)radioState); + radioWasOff_ = true; + radioEnabling_ = true; + bluetooth_set_radio_enabled(btDevice_, true); + } + } else { + if (activeTransport_ == MidiTransport::USB) { + stopUsbMidi(); + } else { + stopBleMidi(); + if (btDevice_) { + unsubscribeBtEvents(); + if (radioWasOff_) bluetooth_set_radio_enabled(btDevice_, false); + device_put(btDevice_); + btDevice_ = nullptr; + } + radioWasOff_ = false; + } + enableTransportButton(); + } +} + +void MidiController::attachEventGroup(struct TaskEventGroup* group) { + eventGroup_ = group; + task_event_group_claim_bit(group, &transportRequestBit_); +} + +void MidiController::processEvents() { + const int request = transportRequest_.exchange(-1); + if (request >= 0) { + applyTransport(request == 1); + } + pollBtEvents(); +} + +void MidiController::sendMidi(uint8_t cmd, uint8_t d1, uint8_t d2) { + uint8_t msg[3] = { cmd, d1, d2 }; + if (bleRunning_) { + struct Device* dev = bluetooth_midi_get(); + if (dev) { + bluetooth_midi_send(dev, msg, 3); + device_put(dev); // bluetooth_midi_get() returns a referenced device + } + } else if (usbRunning_) { + struct Device* dev = usb_midi_device_get(); + if (dev) { + usb_midi_device_send(dev, msg, 3); + device_put(dev); + } + } +} + +void MidiController::stopAllNotes() { + for (int i = 0; i < 128; i++) { + sendMidi(0x80, (uint8_t)i, 0); + } +} + +// --------------------------------------------------------------------------- +// Transport selector +// --------------------------------------------------------------------------- + +void MidiController::updateTransportButton() { + if (!transportIconLbl_) return; + lv_label_set_text(transportIconLbl_, + activeTransport_ == MidiTransport::USB + ? LVGL_ICON_SHARED_USB : LVGL_ICON_SHARED_BLUETOOTH); +} + +void MidiController::showTransportSelect() { + if (transportOverlay_ || bleRunning_ || usbRunning_) return; + transportOverlay_ = buildTransportSelectOverlay(contentArea_); +} + +lv_obj_t* MidiController::buildTransportSelectOverlay(lv_obj_t* parent) { + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontDefault = lvgl_get_text_font(FONT_SIZE_DEFAULT); + const lv_font_t* fontLarge = lvgl_get_text_font(FONT_SIZE_LARGE); + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(root, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(root, MC_BG(), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(root, 32, 0); + lv_obj_set_style_pad_gap(root, 18, 0); + lv_obj_set_scrollable(root, false); + lv_obj_set_clickable(root, true); // swallow taps on empty space + lv_obj_move_foreground(root); + + mcLabel(root, "SELECT MIDI TRANSPORT", fontLarge, MC_PRIMARY()); + mcLabel(root, "Choose how this app sends MIDI.", fontDefault, MC_LABEL()); + + lv_obj_t* row = mcContainer(root); + lv_obj_set_size(row, LV_PCT(100), 280); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_gap(row, 24, 0); + + struct CardSpec { + const char* title; + const char* subtitle; + MidiTransport transport; + }; + static const CardSpec CARDS[] = { + { "BLUETOOTH LE", "Wireless MIDI", MidiTransport::BLE }, + { "USB", "Wired MIDI device", MidiTransport::USB }, + }; + + for (const CardSpec& spec : CARDS) { + bool selected = (activeTransport_ == spec.transport); + + lv_obj_t* card = mcButton(row); + lv_obj_set_size(card, 420, 260); + lv_obj_set_style_bg_color(card, selected ? MC_SURFACE() : MC_PANEL(), 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_radius(card, 20, 0); + lv_obj_set_style_border_width(card, selected ? 3 : 1, 0); + lv_obj_set_style_border_color(card, selected ? MC_PRIMARY() : lv_color_hex(0x223344), 0); + lv_obj_set_style_pad_all(card, 20, 0); + lv_obj_set_style_pad_gap(card, 10, 0); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_user_data(card, (void*)(intptr_t)(spec.transport == MidiTransport::USB ? 1 : 0)); + lv_obj_add_event_cb(card, cbTransportCard, LV_EVENT_CLICKED, this); + + mcLabel(card, spec.title, fontLarge, selected ? MC_PRIMARY() : MC_TEXT()); + mcLabel(card, spec.subtitle, fontSmall, MC_LABEL()); + if (selected) mcLabel(card, "CURRENT", fontSmall, MC_PRIMARY()); + } + + if (currentViewRoot_ != nullptr) { + mcLabel(root, "The toolbar switch starts and stops sending.", fontSmall, MC_LABEL()); + mcBtn(root, "CANCEL", 160, MC_BTN_H, cbTransportCancel, this); + } else { + mcLabel(root, "Choose a transport to open the controller.", fontSmall, MC_LABEL()); + } + + return root; +} + +void MidiController::cbTransportSelect(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->showTransportSelect(); +} + +// The write runs on the kernel's file_ops task: cbTransportCard() runs on the LVGL task, whose +// stack is in PSRAM and must not touch flash. The job owns its data so it can outlive the app. +struct SaveTransportJob { + char path[192]; + int32_t transport; +}; + +static void saveTransportTask(void* context) { + auto* job = static_cast(context); + Preferences* prefs = preferences_open(job->path); + if (prefs) { + preferences_put_int32(prefs, PREF_TRANSPORT, job->transport); + preferences_close(prefs); + } + delete job; +} + +void MidiController::saveTransportSelection() { + auto* job = new SaveTransportJob {}; + if (app_paths_get_user_data_path(APP_ID, SETTINGS_FILE, job->path, sizeof(job->path)) != ERROR_NONE) { + delete job; + return; + } + job->transport = (activeTransport_ == MidiTransport::USB) ? 1 : 0; + if (file_ops_queue(saveTransportTask, job, nullptr) != ERROR_NONE) { + ESP_LOGE(TAG, "Failed to queue transport selection save"); + delete job; + } +} + +void MidiController::cbTransportCard(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + lv_obj_t* card = lv_event_get_target_obj(e); + if (!mc || !card) return; + + intptr_t id = (intptr_t)lv_obj_get_user_data(card); + MidiTransport selected = (id == 1) ? MidiTransport::USB : MidiTransport::BLE; + // Persist an explicit choice - including the very first one, even when it's the default BLE, + // so the chooser doesn't come back on the next launch. + bool shouldSave = (selected != mc->activeTransport_) || !mc->hasSavedTransport_; + mc->activeTransport_ = selected; + mc->hasSavedTransport_ = true; + mc->updateTransportButton(); + if (shouldSave) mc->saveTransportSelection(); + + if (mc->transportOverlay_) { + lv_obj_del(mc->transportOverlay_); + mc->transportOverlay_ = nullptr; + } + + // First launch: there is no view behind the overlay yet - open the home grid. + if (mc->currentViewRoot_ == nullptr) { + mc->currentMode_ = MidiMode::HOME; + mc->buildHomeView(mc->contentArea_); + } +} + +void MidiController::cbTransportCancel(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc || !mc->transportOverlay_) return; + lv_obj_del(mc->transportOverlay_); + mc->transportOverlay_ = nullptr; +} + +// --------------------------------------------------------------------------- +// Mode timer management +// --------------------------------------------------------------------------- + +void MidiController::stopAllModeTimers() { + if (arpTimer_) { lv_timer_delete(arpTimer_); arpTimer_ = nullptr; } + if (lfoTimer_) { lv_timer_delete(lfoTimer_); lfoTimer_ = nullptr; } + if (zenTimer_) { lv_timer_delete(zenTimer_); zenTimer_ = nullptr; } + if (dropTimer_) { lv_timer_delete(dropTimer_); dropTimer_ = nullptr; } + if (rngTimer_) { lv_timer_delete(rngTimer_); rngTimer_ = nullptr; } + if (ragaTimer_) { lv_timer_delete(ragaTimer_); ragaTimer_ = nullptr; } + if (dimTimer_) { lv_timer_delete(dimTimer_); dimTimer_ = nullptr; } + if (baby8Timer_) { lv_timer_delete(baby8Timer_); baby8Timer_ = nullptr; } + if (tb3poTimer_) { lv_timer_delete(tb3poTimer_); tb3poTimer_ = nullptr; } + if (euclidTimer_){ lv_timer_delete(euclidTimer_);euclidTimer_= nullptr; } + // The UI timer only services BEATS' step cursor; don't leave it running + // every 20 ms while another view is open. + if (uiTimer_) { lv_timer_delete(uiTimer_); uiTimer_ = nullptr; } +} + +// --------------------------------------------------------------------------- +// Navigation +// --------------------------------------------------------------------------- + +void MidiController::switchToMode(MidiMode mode) { + // Close the transport chooser if it is still open + if (transportOverlay_) { + lv_obj_del(transportOverlay_); + transportOverlay_ = nullptr; + } + + // Stop active mode timers + stopAllModeTimers(); + + // Stop sequencer if leaving BEATS + if (currentMode_ == MidiMode::BEATS && seq_ && seq_->isPlaying()) { + seq_->stop(); + beats_.playing = false; + } + + // Send All-Notes-Off (CC 123) - single packet instead of 128 note-offs + sendMidi(0xB0, 123, 0); + + // Null all LVGL widget pointers BEFORE deleting the tree, so any callbacks + // fired during deletion (LV_EVENT_DELETE, pending timers) see null pointers. + // Data fields (octave, bpm, scale, etc.) are preserved across mode switches. + zen_.canvas = nullptr; zen_.statusLbl = nullptr; zen_.ballCntLbl = nullptr; zen_.octLbl = nullptr; + for (auto& b : zen_.ballObj) b = nullptr; + + drop_.canvas = nullptr; drop_.statusLbl = nullptr; + drop_.modeLbl = nullptr; drop_.octLbl = nullptr; drop_.modeBtn = nullptr; + for (auto& b : drop_.ballObj) b = nullptr; + for (auto& l : drop_.platLbl) l = nullptr; + + xy_.canvas = nullptr; xy_.indicator = nullptr; + xy_.xCCLbl = nullptr; xy_.yCCLbl = nullptr; xy_.valLbl = nullptr; + + for (auto& k : keys_.whiteKey) k = nullptr; + for (auto& k : keys_.blackKey) k = nullptr; + keys_.pianoArea = nullptr; + keys_.octLbl = nullptr; keys_.keyLbl = nullptr; + keys_.scaleLbl = nullptr; keys_.statusLbl = nullptr; + + beats_.playBtn = nullptr; beats_.playLbl = nullptr; beats_.bpmLbl = nullptr; + for (int t = 0; t < Sequencer::NUM_TRACKS; t++) + for (int s = 0; s < Sequencer::NUM_STEPS; s++) + beats_.stepBtn[t][s] = nullptr; + + for (auto& k : arp_.pianoKey) k = nullptr; + arp_.patternLbl = nullptr; arp_.chordLbl = nullptr; + arp_.speedLbl = nullptr; arp_.bpmLbl = nullptr; + arp_.octLbl = nullptr; arp_.pianoOctLbl = nullptr; + arp_.statusLbl = nullptr; arp_.noteLbl = nullptr; + + for (auto& b : chord_.chordBtn) b = nullptr; + chord_.octLbl = nullptr; chord_.scaleLbl = nullptr; + + for (int r = 0; r < 5; r++) + for (int c = 0; c < 8; c++) + grid_.cell[r][c] = nullptr; + grid_.octLbl = nullptr; grid_.noteLbl = nullptr; + + lfo_.canvas = nullptr; lfo_.startBtn = nullptr; lfo_.startLbl = nullptr; + lfo_.statusLbl = nullptr; lfo_.rateLbl = nullptr; lfo_.waveLbl = nullptr; + lfo_.amountLbl = nullptr; lfo_.ccLbl = nullptr; lfo_.valueLbl = nullptr; + + rng_.playBtn = nullptr; rng_.playLbl = nullptr; rng_.keyLbl = nullptr; + rng_.scaleLbl = nullptr; rng_.minOctLbl = nullptr; rng_.maxOctLbl = nullptr; + rng_.probLbl = nullptr; rng_.bpmLbl = nullptr; rng_.subdivLbl = nullptr; + rng_.noteLbl = nullptr; + rng_.playing = false; rng_.currentNote = -1; + + raga_.ragaLbl = nullptr; raga_.talaLbl = nullptr; raga_.rootLbl = nullptr; + raga_.bpmLbl = nullptr; raga_.statusLbl = nullptr; raga_.playBtn = nullptr; + raga_.playLbl = nullptr; raga_.droneBtn = nullptr; raga_.droneLbl = nullptr; + raga_.ragaBigLbl= nullptr; raga_.talaBigLbl = nullptr; + raga_.playing = false; raga_.droneOn = false; + if (raga_.droneActive) { sendMidi(0x80, raga_.droneNote, 0); raga_.droneActive = false; } + if (raga_.noteActive) { sendMidi(0x80, raga_.currentNote, 0); raga_.noteActive = false; } + + dim_.eqLbl = nullptr; dim_.aLbl = nullptr; dim_.bLbl = nullptr; + dim_.cLbl = nullptr; dim_.dLbl = nullptr; dim_.outLbl = nullptr; + dim_.octLbl = nullptr; dim_.rndLbl = nullptr; dim_.bpmLbl = nullptr; dim_.playLbl = nullptr; + dim_.playing = false; + if (dim_.lastNote >= 0) { sendMidi(0x80, (uint8_t)dim_.lastNote, 0); dim_.lastNote = -1; } + + for (int i = 0; i < 8; i++) { baby8_.stepBtn[i] = nullptr; baby8_.noteLbl[i] = nullptr; } + baby8_.bpmLbl = nullptr; baby8_.swingLbl = nullptr; baby8_.patLbl = nullptr; + baby8_.editNoteLbl = nullptr; baby8_.editVelLbl = nullptr; baby8_.playLbl = nullptr; + baby8_.playing = false; + if (baby8_.lastNote >= 0) { sendMidi(0x80, (uint8_t)baby8_.lastNote, 0); baby8_.lastNote = -1; } + + tb3po_.canvas = nullptr; tb3po_.densityLbl = nullptr; tb3po_.seedLbl = nullptr; + tb3po_.bpmLbl = nullptr; tb3po_.scaleLbl = nullptr; tb3po_.keyLbl = nullptr; + tb3po_.playLbl = nullptr; + tb3po_.playing = false; + if (tb3po_.lastNote >= 0) { sendMidi(0x80, (uint8_t)tb3po_.lastNote, 0); tb3po_.lastNote = -1; } + tb3po_.lastNoteStep = -1; + + euclid_.canvas = nullptr; euclid_.bpmLbl = nullptr; euclid_.stepsLbl = nullptr; + euclid_.eventsLbl= nullptr; euclid_.rotLbl = nullptr; euclid_.noteLbl = nullptr; + euclid_.voiceLbl = nullptr; euclid_.playLbl = nullptr; euclid_.tripLbl = nullptr; + euclid_.voiceTabRow = nullptr; + euclid_.playing = false; + + // Turn off encoder LEDs before building the new view + // (each view's dispatch sets its own colors on first poll) + encoderSetAllLeds(this, 0x000000); + + // On the next encoder poll, seed btnPrev from the live button state + // without firing any actions - prevents a held button from registering + // as a fresh press in the new view. + encoder_.btnSeedNext = true; + + // Discard any touch still in progress so the tap that triggered this mode + // switch doesn't land as a click on a button in the newly built view. + lv_indev_t* indev = lv_indev_get_next(nullptr); + while (indev) { + lv_indev_wait_release(indev); + indev = lv_indev_get_next(indev); + } + + // Destroy current view tree + if (currentViewRoot_) { + lv_obj_del(currentViewRoot_); + currentViewRoot_ = nullptr; + } + + currentMode_ = mode; + lcdSetMode(mode); // marks LCD dirty + lcdUpdate(this); // paint immediately (no-op if LCD not present) + + if (!contentArea_) return; + + switch (mode) { + case MidiMode::HOME: buildHomeView(contentArea_); break; + case MidiMode::KEYS: buildKeysView (this, contentArea_); break; + case MidiMode::BEATS: buildBeatsView(this, contentArea_); break; + case MidiMode::ZEN: buildZenView (this, contentArea_); break; + case MidiMode::DROP: buildDropView (this, contentArea_); break; + case MidiMode::RNG: buildRngView (this, contentArea_); break; + case MidiMode::XY: buildXYPadView(this, contentArea_); break; + case MidiMode::ARP: buildArpView (this, contentArea_); break; + case MidiMode::GRID: buildGridView (this, contentArea_); break; + case MidiMode::CHORD: buildChordView (this, contentArea_); break; + case MidiMode::LFO: buildLfoView (this, contentArea_); break; + case MidiMode::RAGA: buildRagaView (this, contentArea_); break; + case MidiMode::DIMENSIONS: buildDimensionsView(this, contentArea_); break; + case MidiMode::BABY8: buildBaby8View (this, contentArea_); break; + case MidiMode::TB3PO: buildTb3poView (this, contentArea_); break; + case MidiMode::EUCLID: buildEuclidView (this, contentArea_); break; + default: buildHomeView(contentArea_); break; + } + + // BEATS is the only view that needs the shared UI timer. + if (mode == MidiMode::BEATS) { + uiTimer_ = lv_timer_create(cbUiTimer, 20, this); + } + + // Re-arm wait_release after view construction so any touch that was in + // flight when we entered this function cannot land on buttons in the new view. + { + lv_indev_t* indev = lv_indev_get_next(nullptr); + while (indev) { + lv_indev_wait_release(indev); + indev = lv_indev_get_next(indev); + } + } +} + +// --------------------------------------------------------------------------- +// HOME view - 2×5 mode selection grid +// --------------------------------------------------------------------------- + +static const char* MODE_LABELS[] = { + "KEYS", "BEATS", "ZEN", "DROP", "RNG", + "XY", "ARP", "GRID", "CHORD", "LFO", + "RAGA", "DIM", "BABY8", "TB3PO", "EUCLID" +}; + +static lv_color_t MODE_COLOR(int idx) { + static const uint32_t COLORS[] = { + 0xFF3333, 0xFF8800, 0xFFFF00, 0x33CC33, 0x3399FF, + 0xAA33FF, 0xFF33CC, 0x00FFFF, 0xFF9944, 0x99FF44, + 0xFF8844, 0x44BBFF, 0xFF88FF, 0x88FF44, 0xFF4488 + }; + if (idx >= 0 && idx < 15) return lv_color_hex(COLORS[idx]); + return lv_color_hex(0x888888); +} + +void MidiController::cbModeBtn(lv_event_t* e) { + if (!s_instance) return; + int idx = (int)(intptr_t)lv_event_get_user_data(e); + if (idx >= 0 && idx < (int)MidiMode::COUNT) { + s_instance->switchToMode((MidiMode)idx); + } +} + +void MidiController::buildHomeView(lv_obj_t* parent) { + lv_coord_t W = dispW(); + lv_coord_t H = dispH(); + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + styleContainer(root); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, MC_BG(), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(root, 16, 0); + lv_obj_set_style_pad_gap(root, 16, 0); + currentViewRoot_ = root; + + // Button grid - 3 rows of 5, each row flex_grow=1 so buttons always fill height + lv_coord_t gap = 8; + // Root has pad_all 16 on each side; 5 buttons share the rest minus 4 gaps. + lv_coord_t btnW = (W - 16 * 2 - gap * 4) / 5; + const lv_font_t* btnFont = W < 400 ? lvgl_get_text_font(FONT_SIZE_SMALL) + : lvgl_get_text_font(FONT_SIZE_DEFAULT); + + for (int row = 0; row < 3; row++) { + lv_obj_t* rowObj = mcContainer(root); + lv_obj_set_width(rowObj, LV_PCT(100)); + lv_obj_set_flex_grow(rowObj, 1); + lv_obj_set_flex_flow(rowObj, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(rowObj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + styleContainer(rowObj); + lv_obj_set_style_bg_color(rowObj, MC_BG(), 0); + lv_obj_set_style_bg_opa(rowObj, LV_OPA_COVER, 0); + lv_obj_set_style_pad_gap(rowObj, gap, 0); + lv_obj_set_style_pad_all(rowObj, 0, 0); + + for (int col = 0; col < 5; col++) { + int i = row * 5 + col; + lv_obj_t* btn = mcButton(rowObj); + lv_obj_set_size(btn, btnW, LV_PCT(100)); + lv_obj_set_style_bg_color(btn, MC_SURFACE(), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(btn, MODE_COLOR(i), 0); + lv_obj_set_style_border_width(btn, 8, 0); + lv_obj_set_style_radius(btn, 24, 0); + lv_obj_set_style_pad_all(btn, 8, 0); + lv_obj_add_event_cb(btn, cbModeBtn, LV_EVENT_CLICKED, + (void*)(intptr_t)i); + + // Color accent bar at top of button + lv_obj_t* bar = mcContainer(btn); + lv_obj_set_size(bar, LV_PCT(100), 16); + lv_obj_set_style_bg_color(bar, MODE_COLOR(i), 0); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(bar, 0, 0); + lv_obj_set_style_radius(bar, 0, 0); + lv_obj_set_clickable(bar, false); + lv_obj_set_align(bar, LV_ALIGN_TOP_MID); + lv_obj_set_scrollable(bar, false); + + lv_obj_t* lbl = lv_label_create(btn); + lv_label_set_text(lbl, MODE_LABELS[i]); + lv_obj_set_style_text_color(lbl, MC_TEXT(), 0); + lv_obj_set_style_text_font(lbl, btnFont, 0); + lv_obj_center(lbl); + lv_obj_set_clickable(lbl, false); + } + } + (void)H; +} + +// --------------------------------------------------------------------------- +// UI timer - BEATS step indicator +// --------------------------------------------------------------------------- + +void MidiController::cbTransportSwitch(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + bool on = lv_obj_has_state(lv_event_get_target_obj(e), LV_STATE_CHECKED); + mc->handleTransportToggle(on); +} + +void MidiController::cbUiTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + + // BEATS: update step highlight + if (mc->currentMode_ == MidiMode::BEATS && mc->seq_) { + int step = mc->seqPlayStep.load(); + int lastStep = mc->seqLastStep.load(); + if (step != lastStep) { + int prev = lastStep; + mc->seqLastStep.store(step); + + // De-highlight previous step across all tracks + if (prev >= 0 && prev < Sequencer::NUM_STEPS) { + for (int t = 0; t < Sequencer::NUM_TRACKS; t++) { + lv_obj_t* btn = mc->beats_.stepBtn[t][prev]; + if (btn) { + bool active = mc->seq_->getStep(t, prev); + lv_obj_set_style_bg_color(btn, + active ? TRACK_COLOR(t) : MC_SURFACE(), LV_PART_MAIN); + lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN); + } + } + } + // Highlight current step + if (step >= 0 && step < Sequencer::NUM_STEPS) { + for (int t = 0; t < Sequencer::NUM_TRACKS; t++) { + lv_obj_t* btn = mc->beats_.stepBtn[t][step]; + if (btn) { + lv_obj_set_style_bg_color(btn, lv_color_hex(0xFFFFFF), LV_PART_MAIN); + lv_obj_set_style_border_width(btn, 4, LV_PART_MAIN); + lv_obj_set_style_border_color(btn, MC_PRIMARY(), LV_PART_MAIN); + } + } + } + } + + // Check note-offs + uint64_t now = (uint64_t)esp_timer_get_time(); + for (int t = 0; t < Sequencer::NUM_TRACKS; t++) { + if (mc->beats_.noteOffAtUs[t] > 0 && now >= mc->beats_.noteOffAtUs[t]) { + mc->sendMidi(0x80, DRUM_NOTES[t], 0); + mc->beats_.noteOffAtUs[t] = 0; + } + } + } +} + +// --------------------------------------------------------------------------- +// App lifecycle +// --------------------------------------------------------------------------- + +void MidiController::loadSettings() { + char path[192]; + if (app_paths_get_user_data_path(APP_ID, SETTINGS_FILE, path, sizeof(path)) != ERROR_NONE) { + return; + } + Preferences* prefs = preferences_open(path); + if (!prefs) return; + int32_t value = 0; + if (preferences_opt_int32(prefs, PREF_TRANSPORT, &value) && (value == 0 || value == 1)) { + activeTransport_ = (value == 1) ? MidiTransport::USB : MidiTransport::BLE; + hasSavedTransport_ = true; + } + preferences_close(prefs); +} + +void MidiController::detectBleState() { + // Check if BLE MIDI is already active (e.g. system BT was on before app launched). + // If so, mark bleRunning_ so the app is ready to send. No USB + // equivalent - unlike BLE's system-wide radio, USB device mode has no "already running from + // some other app" case worth auto-detecting (only one class can hold the slot at a time, and + // if MIDI already holds it, that's almost certainly this same app from a previous session). + // + // Uses the same ref-counted device_get_first_by_type() lookup handleTransportToggle() uses + // (rather than bluetooth_find_first_ready_device(), which would also work here since this + // check only fires if the radio's already on) so btDevice_ is always ref'd whenever non-null, + // uniformly - every btDevice_ = nullptr elsewhere in this file can then unconditionally + // device_put() first without needing to track which code path originally acquired it. + if (device_get_first_by_type(&BLUETOOTH_TYPE, &btDevice_) == ERROR_NONE) { + enum BtRadioState radioState; + bluetooth_get_radio_state(btDevice_, &radioState); + if (radioState == BT_RADIO_STATE_ON) { + // Radio is already on - register callback and start MIDI immediately. + radioWasOff_ = false; + bluetooth_set_device_name(btDevice_, BLE_DEVICE_NAME); + subscribeBtEvents(); + struct Device* midiDev = bluetooth_midi_get(); + bool midiStarted = false; + if (midiDev) { + midiStarted = (bluetooth_midi_start(midiDev) == ERROR_NONE); + device_put(midiDev); // bluetooth_midi_get() returns a referenced device + } + if (midiStarted) { + bleRunning_ = true; + activeTransport_ = MidiTransport::BLE; + } else { + // MIDI start failed - clean up subscription + unsubscribeBtEvents(); + device_put(btDevice_); + btDevice_ = nullptr; + } + } else { + device_put(btDevice_); + btDevice_ = nullptr; + } + } +} + +void MidiController::createWidgets(lv_obj_t* parent, void* userData) { + auto* mc = static_cast(userData); + s_instance = mc; + mc->createWidgetsImpl(parent); +} + +void MidiController::createWidgetsImpl(lv_obj_t* parent) { + lv_obj_set_scrollable(parent, false); + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(parent, MC_BG(), 0); + lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0); + + // Toolbar + toolbar_ = lvgl_toolbar_create(parent, "Midi Controller"); + lv_obj_align(toolbar_, LV_ALIGN_TOP_MID, 0, 0); + + // Transport selector button (BLE/USB), added before the switch so it sits to its left within + // the toolbar's action area. Opens the full-screen chooser; disabled while the switch is on so + // the transport can't change mid-stream. + transportBtn_ = lvgl_toolbar_add_text_button_action(toolbar_, "BLE", cbTransportSelect, this); + + // Make the action look like a button (the toolbar's default text action has + // no background). Reuse the action's existing label as the icon label: the + // toolbar owns that child and may be drawn while the app is open, so deleting + // it would leave the widget tree with a dangling entry. + transportIconLbl_ = lv_obj_get_child(transportBtn_, 0); + if (transportIconLbl_ != nullptr) { + lv_obj_set_style_text_font(transportIconLbl_, + lvgl_get_shared_icon_default_font(), 0); + lv_obj_set_style_text_color(transportIconLbl_, lv_color_white(), 0); + } + lv_obj_set_size(transportBtn_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(transportBtn_, 10, 0); + lv_obj_set_style_bg_color(transportBtn_, lv_color_hex(0x0A4C78), 0); + lv_obj_set_style_bg_opa(transportBtn_, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(transportBtn_, lv_color_hex(0x8FE8FF), 0); + lv_obj_set_style_border_width(transportBtn_, 2, 0); + lv_obj_set_style_radius(transportBtn_, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_shadow_width(transportBtn_, 0, 0); + lv_obj_set_style_opa(transportBtn_, LV_OPA_60, LV_STATE_DISABLED); + updateTransportButton(); + + switchWidget_ = lvgl_toolbar_add_switch_action(toolbar_); + lv_obj_add_event_cb(switchWidget_, cbTransportSwitch, LV_EVENT_VALUE_CHANGED, this); + + // Main wrapper + mainWrapper_ = mcContainer(parent); + lv_obj_set_width(mainWrapper_, LV_PCT(100)); + lv_obj_set_flex_grow(mainWrapper_, 1); + styleContainer(mainWrapper_); + lv_obj_set_style_bg_color(mainWrapper_, MC_BG(), 0); + lv_obj_set_style_bg_opa(mainWrapper_, LV_OPA_COVER, 0); + + // Content area (fills mainWrapper) + contentArea_ = mcContainer(mainWrapper_); + lv_obj_set_size(contentArea_, LV_PCT(100), LV_PCT(100)); + styleContainer(contentArea_); + lv_obj_set_style_bg_color(contentArea_, MC_BG(), 0); + lv_obj_set_style_bg_opa(contentArea_, LV_OPA_COVER, 0); + + // Create Sequencer + seq_ = std::make_unique(); + seq_->onStep = [](int /*step*/, int track) { + if (!s_instance) return; + static const uint32_t NOTE_OFF_US[] = { 200000, 150000, 50000, 300000 }; + s_instance->sendMidi(0x90, DRUM_NOTES[track], 100); + s_instance->beats_.noteOffAtUs[track] = + (uint64_t)esp_timer_get_time() + NOTE_OFF_US[track]; + }; + seq_->onTick = [](int step) { + if (s_instance) s_instance->seqPlayStep.store(step); + }; + + // detectBleState() found a BLE MIDI session that is already running (e.g. system BT was on + // before the app launched): the switch shows it + if (bleRunning_) { + lv_obj_add_state(switchWidget_, LV_STATE_CHECKED); + lv_obj_add_state(transportBtn_, LV_STATE_DISABLED); + } + + // Start HOME view, or show the transport chooser first (unless BLE was already running, or a + // transport was chosen in an earlier session). + currentMode_ = MidiMode::HOME; + if (bleRunning_ || hasSavedTransport_) { + buildHomeView(contentArea_); + } else { + showTransportSelect(); + } + + // 8Encoder (optional hardware - graceful no-op if not present) + encoderInit(this); +} + +void MidiController::teardown() { + // Stop 8Encoder poll + turn off LEDs + encoderStop(this); + + // Stop mode timers (including the BEATS UI timer) + stopAllModeTimers(); + + // Stop sequencer + if (seq_) { + seq_->stop(); + seq_.reset(); + } + + // MIDI transport + BLE radio cleanup (MediaKeys pattern). + radioEnabling_ = false; + stopBleMidi(); + stopUsbMidi(); + if (btDevice_) { + unsubscribeBtEvents(); + if (radioWasOff_) bluetooth_set_radio_enabled(btDevice_, false); + device_put(btDevice_); + btDevice_ = nullptr; + } + radioWasOff_ = false; + + resetAllState(); + s_instance = nullptr; +} + +void MidiController::resetAllState() { + toolbar_ = nullptr; + transportBtn_ = nullptr; + transportIconLbl_ = nullptr; + transportOverlay_ = nullptr; + mainWrapper_ = nullptr; + switchWidget_ = nullptr; + contentArea_ = nullptr; + currentViewRoot_ = nullptr; + + keys_ = {}; keys_.octave = 4; + for (auto& n : keys_.lastNote) n = -1; + beats_ = {}; beats_.bpm = 120; + xy_ = {}; xy_.xCC = 74; xy_.yCC = 71; xy_.xVal = 64; xy_.yVal = 64; + arp_ = {}; arp_.bpm = 120; arp_.speedIdx = 1; arp_.pianoOctave = 4; + arp_.triggeredNote = -1; arp_.lastNote = -1; + grid_ = {}; for (auto& n : grid_.pressedNote) n = -1; + chord_ = {}; chord_.octave = 4; + lfo_ = {}; lfo_.rate = 1.0f; lfo_.amount = 64; lfo_.ccTarget = 1; lfo_.lastValue = 64; + zen_ = {}; zen_.octave = 4; + drop_ = {}; drop_.octave = 4; + rng_ = {}; rng_.bpm = 120; rng_.subdivision = 0; + rng_.minOctave = 3; rng_.maxOctave = 6; rng_.probability = 50; + raga_ = {}; raga_.rootNote = 48; raga_.bpm = 120; + dim_ = {}; dim_.equation = 1; dim_.a = 50; dim_.b = 50; dim_.c = 50; dim_.d = 50; + dim_.bpm = 120; dim_.t2 = 100; dim_.gran = 1.0f; + dim_.xOff = 64; dim_.yOff = 64; dim_.lastNote = -1; + baby8_ = {}; baby8_.bpm = 120; baby8_.swing = 50; baby8_.lastNote = -1; + for (int i = 0; i < 8; i++) { baby8_.steps[i].note = 60 + i; baby8_.steps[i].velocity = 100; baby8_.steps[i].gate = true; } + for (int p = 0; p < 8; p++) for (int s = 0; s < 8; s++) { baby8_.patterns[p][s].note = 60 + s; baby8_.patterns[p][s].velocity = 100; baby8_.patterns[p][s].gate = true; } + tb3po_ = {}; tb3po_.density = 8; tb3po_.bpm = 120; tb3po_.octave = 3; tb3po_.lastNote = -1; + euclid_= {}; euclid_.bpm = 120; +} diff --git a/Apps/MidiController/main/Source/MidiController.h b/Apps/MidiController/main/Source/MidiController.h new file mode 100644 index 0000000..b4ae99d --- /dev/null +++ b/Apps/MidiController/main/Source/MidiController.h @@ -0,0 +1,576 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MidiScales.h" +#include "Sequencer.h" +#include "UiKit.h" + +// --------------------------------------------------------------------------- +// Transport selection - which MIDI output carries sendMidi()'s bytes +// --------------------------------------------------------------------------- + +enum class MidiTransport { + BLE = 0, + USB = 1, +}; + +// --------------------------------------------------------------------------- +// Mode enum +// --------------------------------------------------------------------------- + +enum class MidiMode { + HOME = -1, + KEYS = 0, + BEATS = 1, + ZEN = 2, + DROP = 3, + RNG = 4, + XY = 5, + ARP = 6, + GRID = 7, + CHORD = 8, + LFO = 9, + RAGA = 10, + DIMENSIONS = 11, + BABY8 = 12, + TB3PO = 13, + EUCLID = 14, + COUNT = 15, +}; + +// --------------------------------------------------------------------------- +// Forward declarations for per-view build functions (each in ViewXxx.cpp) +// --------------------------------------------------------------------------- + +class MidiController; +void buildKeysView (MidiController* mc, lv_obj_t* parent); +void buildBeatsView (MidiController* mc, lv_obj_t* parent); +void buildXYPadView (MidiController* mc, lv_obj_t* parent); +void buildArpView (MidiController* mc, lv_obj_t* parent); +void buildGridView (MidiController* mc, lv_obj_t* parent); +void buildChordView (MidiController* mc, lv_obj_t* parent); +void buildLfoView (MidiController* mc, lv_obj_t* parent); +void buildZenView (MidiController* mc, lv_obj_t* parent); +void buildDropView (MidiController* mc, lv_obj_t* parent); +void buildRngView (MidiController* mc, lv_obj_t* parent); +void buildRagaView (MidiController* mc, lv_obj_t* parent); +void buildDimensionsView(MidiController* mc, lv_obj_t* parent); +void buildBaby8View (MidiController* mc, lv_obj_t* parent); +void buildTb3poView (MidiController* mc, lv_obj_t* parent); +void buildEuclidView (MidiController* mc, lv_obj_t* parent); +void euclidOnEncoderUpdate (MidiController* mc); // regen patterns + refresh UI after knob change + +// Encoder action entry points - implemented in each View*.cpp +void ragaTogglePlay (MidiController* mc); +void ragaToggleDrone (MidiController* mc); +void dimTogglePlay (MidiController* mc); +void dimReset (MidiController* mc); +void dimChangeOctave (MidiController* mc, int delta); +void dimChangeRandom (MidiController* mc, int delta); +void tb3TogglePlay (MidiController* mc); +void tb3ChangeScale (MidiController* mc, int delta); +void tb3Regen (MidiController* mc); +void tb3Redraw (MidiController* mc); // repaint piano roll after key/octave changes +void baby8UpdateStepColors(MidiController* mc); // refresh step colours after an encoder gate change + +// 8Encoder hardware support (EncoderInput.cpp) +void encoderInit (MidiController* mc); // detect hardware, start poll timer +void encoderStop (MidiController* mc); // stop poll timer, turn LEDs off +void encoderSetLed(MidiController* mc, uint8_t idx, uint32_t rgb); // set one LED +void encoderSetAllLeds(MidiController* mc, uint32_t rgb); // set all LEDs same color + +// --------------------------------------------------------------------------- +// MidiController +// --------------------------------------------------------------------------- + +class MidiController final { +public: + // window_manager_create()'s WindowCreateWidgetsFn - userData is the MidiController* for this instance. + static void createWidgets(lv_obj_t* parent, void* userData); + + // Call once, after the window has been torn down. + void teardown(); + + // Send a 3-byte MIDI message via BLE MIDI (no-op if not connected) + void sendMidi(uint8_t cmd, uint8_t d1, uint8_t d2); + + // Send Note Off for all 128 notes on channel 1 + void stopAllNotes(); + + // Switch to a mode (or HOME). Stops timers, sends All-Notes-Off, swaps view. + void switchToMode(MidiMode mode); + + // BLE state. Written by the app's own task, read by sendMidi() on any task. + std::atomic bleRunning_ {false}; + + // Called by main() on the app's own task. BLE and USB MIDI start and stop there, as they do + // flash and NVS work, which the LVGL task can't do: its stack is in external RAM. + // attachEventGroup() claims the bit that the LVGL task signals transport requests with. + void attachEventGroup(struct TaskEventGroup* group); + // Restores the transport chosen in an earlier session (selection only, not its on/off state). + // Call before detectBleState() so a BLE session that is already running still wins. + void loadSettings(); + // Picks up a BLE MIDI session that is already running, before the window is created + void detectBleState(); + // Handles the pending transport request and Bluetooth events. Call when the event group wakes. + void processEvents(); + + // Active mode timers - each view creates its own, stored here so + // switchToMode() can delete them before swapping the view tree. + lv_timer_t* arpTimer_ = nullptr; + lv_timer_t* lfoTimer_ = nullptr; + lv_timer_t* zenTimer_ = nullptr; + lv_timer_t* dropTimer_ = nullptr; + lv_timer_t* rngTimer_ = nullptr; + lv_timer_t* ragaTimer_ = nullptr; + lv_timer_t* dimTimer_ = nullptr; + lv_timer_t* baby8Timer_ = nullptr; + lv_timer_t* tb3poTimer_ = nullptr; + lv_timer_t* euclidTimer_ = nullptr; + lv_timer_t* encoderTimer_ = nullptr; // 8Encoder/LCD poll (100 ms) + + // M5Stack 8Encoder state (I2C address 0x41, optional hardware) + struct EncoderState { + bool present = false; // true if detected at 0x41 on startup + int32_t counter[8] = {}; // last read absolute counter per knob + bool btnPrev[8] = {}; // previous button state for edge detection + bool btnSeedNext = false; // if true, next poll seeds btnPrev without firing actions + // LED colors per knob (0xRRGGBB, 0 = off) + uint32_t ledColor[8] = {}; + } encoder_; + + // Drum sequencer (owned here, accessed by ViewBeats) + std::unique_ptr seq_; + std::atomic seqPlayStep{-1}; + std::atomic seqLastStep{-1}; // previous step for highlight refresh + + // Singleton for static callbacks + static MidiController* s_instance; + + // Current view root - set by each buildXxxView, deleted in switchToMode + lv_obj_t* currentViewRoot_ = nullptr; + + // ----------------------------------------------------------------------- + // Per-mode state (inner structs - widget pointers nulled by switchToMode/teardown) + // ----------------------------------------------------------------------- + + struct KeysState { + int octave = 4, scale = 0, key = 0; + // One "currently sounding note" per touch slot (see lvgl_pointer_get_slot_index() in + // ViewKeys.cpp's cbPianoPressing/cbPianoReleased) rather than a single lastNote - lets + // multiple fingers each independently slide/hold their own key. -1 = no note held in + // that slot. Explicitly initialized (not just `= {}`, which would zero-init to note 0 - + // a real playable note, not "empty") both here and again in resetAllState(). The literal + // count below must track LVGL_POINTER_MAX_SLOTS - static_assert catches drift. + int lastNote[LVGL_POINTER_MAX_SLOTS] = { -1, -1, -1, -1, -1 }; + static_assert(LVGL_POINTER_MAX_SLOTS == 5, "update the lastNote initializer above to match"); + // Single octave visible at a time (OCT-/OCT+ shifts it), like SoundBench. + lv_obj_t* pianoArea = nullptr; + lv_obj_t* whiteKey[7] = {}; + lv_obj_t* blackKey[5] = {}; + lv_obj_t* statusLbl = nullptr; + lv_obj_t* scaleLbl = nullptr; + lv_obj_t* keyLbl = nullptr; + lv_obj_t* octLbl = nullptr; + } keys_; + + struct BeatsState { + int bpm = 120; + bool playing = false; + lv_obj_t* stepBtn[Sequencer::NUM_TRACKS][Sequencer::NUM_STEPS] = {}; + lv_obj_t* playBtn = nullptr; + lv_obj_t* playLbl = nullptr; + lv_obj_t* bpmLbl = nullptr; + // per-track note-off scheduling (filled in onStep callback) + uint64_t noteOffAtUs[Sequencer::NUM_TRACKS] = {}; + } beats_; + + struct XYState { + int xCC = 74, yCC = 71; + int xVal = 64, yVal = 64; + lv_obj_t* canvas = nullptr; + lv_obj_t* indicator = nullptr; // circle widget tracking touch position + lv_obj_t* xCCLbl = nullptr; + lv_obj_t* yCCLbl = nullptr; + lv_obj_t* valLbl = nullptr; + // canvas pixel dimensions (set after layout) + int canvasW = 0, canvasH = 0; + } xy_; + + struct ArpState { + int bpm = 120; + int speedIdx = 1; // 0=quarter 1=eighth 2=sixteenth 3=32nd + int pattern = 0; // 0=UP 1=DOWN 2=UPDN 3=RAND 4=CHANCE + int chordType = 0; // 0=MAJ 1=MIN 2=7TH + int octaveRange = 2; + int pianoOctave = 4; + int triggeredNote = -1; // root note being arped (-1=stopped) + bool isPlaying = false; + int arpStep = 0; + int arpDir = 1; // +1 or -1 for UPDN + lv_obj_t* pianoKey[12] = {}; + lv_obj_t* statusLbl = nullptr; + lv_obj_t* noteLbl = nullptr; + lv_obj_t* patternLbl = nullptr; + lv_obj_t* chordLbl = nullptr; + lv_obj_t* speedLbl = nullptr; + lv_obj_t* bpmLbl = nullptr; + lv_obj_t* octLbl = nullptr; + lv_obj_t* pianoOctLbl = nullptr; + int lastNote = -1; // for note-off + } arp_; + + struct GridState { + int octave = 3; + // One held note per touch slot, same pattern as KeysState::lastNote - lets multiple + // fingers each hold their own grid cell instead of the last touch stealing the others'. + // -1 = no note held in that slot. + int pressedNote[LVGL_POINTER_MAX_SLOTS] = { -1, -1, -1, -1, -1 }; + static_assert(LVGL_POINTER_MAX_SLOTS == 5, "update the pressedNote initializer above to match"); + lv_obj_t* cell[5][8] = {}; + lv_obj_t* octLbl = nullptr; + lv_obj_t* noteLbl = nullptr; + } grid_; + + struct ChordState { + int octave = 4; + int scale = 0; + int key = 0; + bool chordPlaying[8] = {}; + // up to 4 notes per chord + int chordNotes[8][4] = {}; + lv_obj_t* chordBtn[8] = {}; + lv_obj_t* octLbl = nullptr; + lv_obj_t* scaleLbl = nullptr; + } chord_; + + struct LfoState { + float rate = 1.0f; // Hz + int amount = 64; // 0-127 scale factor + int ccTarget = 1; // CC number (0-127) when not in pitch mode + bool pitchMode = false; + bool running = false; + int waveform = 0; // 0=SINE 1=TRI 2=SQR 3=SAW + float phase = 0.0f; // 0.0 - 1.0 + int lastValue = 64; + uint64_t lastUpdateUs = 0; + int updateCounter = 0; // throttle label updates + lv_obj_t* canvas = nullptr; + lv_obj_t* statusLbl = nullptr; + lv_obj_t* rateLbl = nullptr; + lv_obj_t* amountLbl = nullptr; + lv_obj_t* ccLbl = nullptr; + lv_obj_t* waveLbl = nullptr; + lv_obj_t* valueLbl = nullptr; + lv_obj_t* startBtn = nullptr; + lv_obj_t* startLbl = nullptr; + int canvasW = 0, canvasH = 0; + } lfo_; + + struct ZenState { + struct Ball { + float x, y, vx, vy; + int radius; + uint32_t color; + bool active = false; + }; + static constexpr int MAX_BALLS = 4; + Ball balls[MAX_BALLS] = {}; + lv_obj_t* ballObj[MAX_BALLS] = {}; // LVGL circle widgets for each ball + int numBalls = 0; + int scale = 0, key = 0, octave = 4; + // Wall flash timers (ms, 0 = no flash) + uint32_t wallFlashUntilMs[24] = {}; // TOP_SEGS(8)+BOTTOM_SEGS(8)+SIDE_SEGS*2(8) = 24 + // Pending note-offs: note value + time to send (0 = empty slot) + static constexpr int MAX_PENDING_OFFS = 8; + uint8_t pendingOffNote[MAX_PENDING_OFFS] = {}; + uint32_t pendingOffMs [MAX_PENDING_OFFS] = {}; + lv_obj_t* canvas = nullptr; + int canvasW = 0, canvasH = 0; + lv_obj_t* statusLbl = nullptr; + lv_obj_t* ballCntLbl = nullptr; + lv_obj_t* octLbl = nullptr; + } zen_; + + struct DropState { + struct DropBall { + float x, y, vx, vy; + bool active = false; + uint32_t spawnedMs = 0; + }; + struct Platform { + float x, y, w; + float angle; // radians, tilt + int note; + }; + static constexpr int MAX_BALLS = 8; + static constexpr int MAX_PLATFORMS = 6; + DropBall balls[MAX_BALLS] = {}; + lv_obj_t* ballObj[MAX_BALLS] = {}; // LVGL circle widgets + lv_obj_t* platLbl[MAX_PLATFORMS] = {}; // LVGL label widgets on canvas + Platform platforms[MAX_PLATFORMS] = {}; + int numPlatforms = 0; + int scale = 0, key = 0, octave = 4; + bool editMode = false; + lv_obj_t* canvas = nullptr; + int canvasW = 0, canvasH = 0; + lv_obj_t* statusLbl = nullptr; + lv_obj_t* modeLbl = nullptr; // shows "DROP" or "EDIT" + lv_obj_t* octLbl = nullptr; + lv_obj_t* modeBtn = nullptr; + } drop_; + + struct RngState { + int bpm = 120; + int subdivision = 0; // index into SUBDIV_NAMES: 0=1/4 1=1/8 2=1/16 + int minOctave = 3; + int maxOctave = 6; + int probability = 50; // 0-100% + int scaleIdx = 0; + int rootKey = 0; // 0-11 + bool playing = false; + int currentNote = -1; + uint64_t nextNoteUs = 0; + uint64_t noteOffUs = 0; + lv_obj_t* noteLbl = nullptr; + lv_obj_t* playBtn = nullptr; + lv_obj_t* playLbl = nullptr; + lv_obj_t* bpmLbl = nullptr; + lv_obj_t* probLbl = nullptr; + lv_obj_t* scaleLbl = nullptr; + lv_obj_t* keyLbl = nullptr; + lv_obj_t* minOctLbl = nullptr; + lv_obj_t* maxOctLbl = nullptr; + lv_obj_t* subdivLbl = nullptr; + } rng_; + + // ----------------------------------------------------------------------- + // New mode states + // ----------------------------------------------------------------------- + + struct RagaState { + int raga = 0; // 0-7 + int tala = 0; // 0-2 + int rootNote = 48; + int bpm = 120; + bool playing = false; + bool droneOn = false; + // Markov / phrase state + int8_t phrase[128] = {}; + int phraseLen = 0, phraseIdx = 0; + int8_t history[2] = {0, 0}; // 2nd-order Markov history + uint32_t nextNoteMs = 0; + uint32_t noteOffMs = 0; + bool noteActive = false; + uint8_t currentNote = 0; + bool droneActive = false; + uint8_t droneNote = 0; + int talaBeat = 0; + uint32_t noteInterval = 150; + uint32_t noteDuration = 120; + // UI + lv_obj_t* ragaLbl = nullptr; + lv_obj_t* talaLbl = nullptr; + lv_obj_t* rootLbl = nullptr; + lv_obj_t* bpmLbl = nullptr; + lv_obj_t* statusLbl = nullptr; + lv_obj_t* playBtn = nullptr; + lv_obj_t* playLbl = nullptr; + lv_obj_t* droneBtn = nullptr; + lv_obj_t* droneLbl = nullptr; + lv_obj_t* ragaBigLbl = nullptr; // right-side display + lv_obj_t* talaBigLbl = nullptr; + } raga_; + + struct DimensionsState { + int equation = 1; // 1-20 + float a = 50, b = 50, c = 50, d = 50; + float t = 0.0f; + float t2 = 100.0f; + float gran = 1.0f; // t step per tick + float rnd = 0.0f; + float xOff = 64.0f, yOff = 64.0f; + float px = 0, py = 0, pz = 0; + int octave = 0; + int bpm = 120; + bool playing = false; + int lastNote = -1; + uint32_t tickMs = 0; // next tick timestamp + uint32_t noteOffMs = 0; + // UI + lv_obj_t* eqLbl = nullptr; + lv_obj_t* aLbl = nullptr; + lv_obj_t* bLbl = nullptr; + lv_obj_t* cLbl = nullptr; + lv_obj_t* dLbl = nullptr; + lv_obj_t* outLbl = nullptr; // "px:N py:N pz:N t:N" + lv_obj_t* octLbl = nullptr; + lv_obj_t* rndLbl = nullptr; + lv_obj_t* bpmLbl = nullptr; + lv_obj_t* playLbl = nullptr; + } dim_; + + struct Baby8State { + struct Step { int note = 60; int velocity = 100; bool gate = true; }; + Step steps[8] = {}; + Step patterns[8][8] = {}; + int currentStep = 0; + int selectedStep = 0; + bool playing = false; + int bpm = 120; + int swing = 50; // 50-80 + int patternIdx = 0; + int lastNote = -1; + uint32_t nextStepMs = 0; + // UI + lv_obj_t* stepBtn[8] = {}; + lv_obj_t* noteLbl[8] = {}; + lv_obj_t* bpmLbl = nullptr; + lv_obj_t* swingLbl = nullptr; + lv_obj_t* patLbl = nullptr; + lv_obj_t* editNoteLbl = nullptr; + lv_obj_t* editVelLbl = nullptr; + lv_obj_t* playLbl = nullptr; + } baby8_; + + struct Tb3poState { + struct Step { + int note = 0; + bool oct_up = false, oct_down = false; + bool accent = false, slide = false, gate = false; + }; + Step steps[32] = {}; + uint32_t seed = 12345; + int density = 8; // 0-16 + int scale = 0; + int key = 0; + int octave = 3; + int currentStep = 0; + bool playing = false; + int bpm = 120; + int lastNote = -1; + int lastNoteStep = -1; // step that owns lastNote (slide bookkeeping) + uint32_t nextStepMs = 0; + uint32_t noteOffMs = 0; + // UI + lv_obj_t* canvas = nullptr; + lv_obj_t* densityLbl = nullptr; + lv_obj_t* seedLbl = nullptr; + lv_obj_t* bpmLbl = nullptr; + lv_obj_t* scaleLbl = nullptr; + lv_obj_t* keyLbl = nullptr; + lv_obj_t* playLbl = nullptr; + int canvasW = 0, canvasH = 0; + } tb3po_; + + struct EuclidState { + struct Voice { + int steps = 16; + int events = 4; + int rotation = 0; + int note = 36; + bool pattern[64] = {}; + }; + Voice voice[8] = {}; + uint32_t currentStep = 0; // free-running master clock (each voice uses modulo its own steps) + bool playing = false; + int bpm = 120; + bool triplet = false; + int selectedVoice = 0; + uint32_t nextStepMs = 0; + // UI + lv_obj_t* canvas = nullptr; + lv_obj_t* bpmLbl = nullptr; + lv_obj_t* stepsLbl = nullptr; + lv_obj_t* eventsLbl = nullptr; + lv_obj_t* rotLbl = nullptr; + lv_obj_t* noteLbl = nullptr; + lv_obj_t* voiceLbl = nullptr; + lv_obj_t* playLbl = nullptr; + lv_obj_t* tripLbl = nullptr; // TRIP ON / TRIP OFF + lv_obj_t* voiceTabRow = nullptr; // tab button row - for encoder voice-select highlight + int canvasW = 0, canvasH = 0; + } euclid_; + + // Current mode - read by EncoderInput.cpp for per-mode dispatch + MidiMode currentMode_ = MidiMode::HOME; + +public: + AppInstanceId appInstanceId = 0; + WindowId window = 0; + +private: + lv_obj_t* toolbar_ = nullptr; + lv_obj_t* transportBtn_ = nullptr; // toolbar button: current transport - opens the selector + lv_obj_t* transportIconLbl_ = nullptr; // transport icon inside that button + lv_obj_t* transportOverlay_ = nullptr; // full-content transport chooser (null when closed) + lv_obj_t* mainWrapper_ = nullptr; + lv_obj_t* contentArea_ = nullptr; + lv_obj_t* switchWidget_ = nullptr; + lv_timer_t* uiTimer_ = nullptr; // 20ms BEATS step-cursor/note-off refresh + + // Which transport sendMidi() targets. Chosen in the transport selector before the switch is + // turned on; only one of bleRunning_/usbRunning_ is ever true. + MidiTransport activeTransport_ = MidiTransport::BLE; + // True once a transport has been chosen, either from the selector this session or from the + // settings file. When set, the app opens the home view instead of the chooser on launch. + bool hasSavedTransport_ = false; + + // BLE radio management (MediaKeys pattern) + struct Device* btDevice_ = nullptr; + bool radioWasOff_ = false; // true if we turned the radio on (must turn it back off) + bool radioEnabling_ = false; // true while waiting for BT_RADIO_STATE_ON + + // The app's event group, owned by main(). Bluetooth events and transport requests wake its loop. + struct TaskEventGroup* eventGroup_ = nullptr; + uint32_t transportRequestBit_ = 0; + // -1: none, 0: turn the transport off, 1: turn it on. Set by the LVGL task, taken by the app's task. + std::atomic transportRequest_ {-1}; + struct BtEventSubscription btEventSub_ {}; + bool btEventSubscribed_ = false; + + // USB MIDI state - no radio-enable dance needed, claim/release is synchronous. + std::atomic usbRunning_ {false}; + + void handleTransportToggle(bool enabled); // LVGL task: updates the UI and requests the transport change + void applyTransport(bool enabled); // App task: starts or stops the transport + void failTransportStart(); // Switches the UI back to off + void enableTransportButton(); + void startBleMidi(); // called once radio is confirmed ON + void stopBleMidi(); // sends all-notes-off and stops BLE MIDI + void startUsbMidi(); + void stopUsbMidi(); // sends all-notes-off and stops USB MIDI + + void showTransportSelect(); // opens the BLE/USB chooser over the current view + void updateTransportButton(); // refresh the toolbar button label + lv_obj_t* buildTransportSelectOverlay(lv_obj_t* parent); + // Queues the chosen transport for the kernel file_ops task (this is called from the LVGL task). + void saveTransportSelection(); + + void createWidgetsImpl(lv_obj_t* parent); + void buildHomeView(lv_obj_t* parent); + void stopAllModeTimers(); + void resetAllState(); + + void handleBtEvent(const struct BtEvent& event); + void pollBtEvents(); + void subscribeBtEvents(); + void unsubscribeBtEvents(); + + static void cbUiTimer(lv_timer_t* t); + static void cbModeBtn(lv_event_t* e); + static void cbTransportSwitch(lv_event_t* e); + static void cbTransportSelect(lv_event_t* e); // toolbar transport button + static void cbTransportCard(lv_event_t* e); // card inside the transport chooser + static void cbTransportCancel(lv_event_t* e); +}; diff --git a/Apps/MidiController/main/Source/MidiScales.h b/Apps/MidiController/main/Source/MidiScales.h new file mode 100644 index 0000000..a6336fe --- /dev/null +++ b/Apps/MidiController/main/Source/MidiScales.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// Scale definitions +// --------------------------------------------------------------------------- + +struct MidiScale { + const char* name; + uint8_t intervals[12]; + int numNotes; +}; + +static constexpr MidiScale MIDI_SCALES[] = { + { "Major", { 0, 2, 4, 5, 7, 9, 11 }, 7 }, + { "Minor", { 0, 2, 3, 5, 7, 8, 10 }, 7 }, + { "Dorian", { 0, 2, 3, 5, 7, 9, 10 }, 7 }, + { "Penta", { 0, 2, 4, 7, 9 }, 5 }, + { "Blues", { 0, 3, 5, 6, 7, 10 }, 6 }, + { "Chrome", { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }, 12 }, +}; +static constexpr int NUM_MIDI_SCALES = sizeof(MIDI_SCALES) / sizeof(MIDI_SCALES[0]); + +// Returns the MIDI note number for a given scale degree, octave, and key transpose. +// degree can exceed numNotes - it wraps and increments the octave. +inline int getNoteInScale(int scaleIdx, int degree, int octave, int keyTranspose = 0) { + scaleIdx = ((scaleIdx % NUM_MIDI_SCALES) + NUM_MIDI_SCALES) % NUM_MIDI_SCALES; + const MidiScale& s = MIDI_SCALES[scaleIdx]; + int actualDegree = ((degree % s.numNotes) + s.numNotes) % s.numNotes; + int octaveOffset = (degree >= 0) ? (degree / s.numNotes) : ((degree - s.numNotes + 1) / s.numNotes); + int note = 60 + s.intervals[actualDegree] + ((octave - 4 + octaveOffset) * 12) + keyTranspose; + // Clamp to valid MIDI range + if (note < 0) note = 0; + if (note > 127) note = 127; + return note; +} + +// Returns true if a given MIDI pitch class (0-11) is in the specified scale + key. +inline bool isNoteInScale(int scaleIdx, int keyTranspose, int midiNote) { + scaleIdx = ((scaleIdx % NUM_MIDI_SCALES) + NUM_MIDI_SCALES) % NUM_MIDI_SCALES; + const MidiScale& s = MIDI_SCALES[scaleIdx]; + int pitchClass = ((midiNote - keyTranspose) % 12 + 12) % 12; + for (int i = 0; i < s.numNotes; i++) { + if (s.intervals[i] == pitchClass) return true; + } + return false; +} + +// Returns true if the pitch class is the root note. +inline bool isRootNote(int keyTranspose, int midiNote) { + return (((midiNote - keyTranspose) % 12 + 12) % 12) == 0; +} + +// Fills buf with a sharp-based note name like "C4", "C#4", "A#3", etc. +inline const char* midiNoteName(int note, char* buf, int bufLen) { + static const char* NAMES[] = { "C","C#","D","D#","E","F","F#","G","G#","A","A#","B" }; + if (note < 0) note = 0; + int octave = (note / 12) - 1; // MIDI 60 = C4 (scientific pitch notation) + snprintf(buf, bufLen, "%s%d", NAMES[note % 12], octave); + return buf; +} + +// Returns short note name without octave (e.g. "C#") +inline const char* midiNoteClass(int note, char* buf, int bufLen) { + static const char* NAMES[] = { "C","C#","D","D#","E","F","F#","G","G#","A","A#","B" }; + if (note < 0) note = 0; + snprintf(buf, bufLen, "%s", NAMES[note % 12]); + return buf; +} + +// Key names for root note display (C, C#, D, ...) +static constexpr const char* KEY_NAMES[] = { + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" +}; diff --git a/Apps/MidiController/main/Source/Sequencer.cpp b/Apps/MidiController/main/Source/Sequencer.cpp new file mode 100644 index 0000000..9d86dcd --- /dev/null +++ b/Apps/MidiController/main/Source/Sequencer.cpp @@ -0,0 +1,107 @@ +#include "Sequencer.h" + +#include +#include +#include + +static constexpr auto* TAG = "MidiSeq"; + +Sequencer::Sequencer() { + memset(steps_, 0, sizeof(steps_)); + + esp_timer_create_args_t args = { + .callback = timerCb, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "midi_seq_tick", + .skip_unhandled_events = true, + }; + if (esp_timer_create(&args, &timer_) != ESP_OK) { + ESP_LOGE(TAG, "Failed to create timer"); + timer_ = nullptr; + } +} + +Sequencer::~Sequencer() { + stop(); + if (timer_) { + esp_timer_delete(timer_); + timer_ = nullptr; + } +} + +void Sequencer::start() { + if (playing_.load()) return; + currentStep.store(0); + playing_.store(true); + restartTimer(); +} + +void Sequencer::stop() { + if (!playing_.load()) return; + playing_.store(false); + if (timer_) esp_timer_stop(timer_); + currentStep.store(-1); +} + +void Sequencer::setBpm(float bpm) { + if (bpm < 40.0f) bpm = 40.0f; + if (bpm > 300.0f) bpm = 300.0f; + bpm_.store(bpm); + if (playing_.load()) restartTimer(); +} + +void Sequencer::toggleStep(int track, int step) { + if (track >= 0 && track < NUM_TRACKS && step >= 0 && step < NUM_STEPS) { + std::lock_guard lock(mutex_); + steps_[track][step] = !steps_[track][step]; + } +} + +bool Sequencer::getStep(int track, int step) const { + if (track >= 0 && track < NUM_TRACKS && step >= 0 && step < NUM_STEPS) { + std::lock_guard lock(const_cast(mutex_)); + return steps_[track][step]; + } + return false; +} + +void Sequencer::clearAll() { + // Stop playback first so the timer callback cannot observe a partially-cleared array + stop(); + std::lock_guard lock(mutex_); + memset(steps_, 0, sizeof(steps_)); +} + +void Sequencer::restartTimer() { + if (!timer_) return; + esp_timer_stop(timer_); + // 16th-note interval: 60s / BPM / 4 * 1e6 us = 15,000,000 / BPM + uint64_t intervalUs = (uint64_t)(15000000.0f / bpm_.load()); + if (esp_timer_start_periodic(timer_, intervalUs) != ESP_OK) { + ESP_LOGE(TAG, "Failed to start sequencer timer"); + } +} + +void Sequencer::timerCb(void* arg) { + static_cast(arg)->onTimerFired(); +} + +void Sequencer::onTimerFired() { + if (!playing_.load()) return; + + int step = currentStep.load(); + + if (onStep) { + std::lock_guard lock(mutex_); + for (int t = 0; t < NUM_TRACKS; t++) { + if (steps_[t][step]) { + onStep(step, t); + } + } + } + + currentStep.store((step + 1) % NUM_STEPS); + + if (onTick) onTick(step); +} diff --git a/Apps/MidiController/main/Source/Sequencer.h b/Apps/MidiController/main/Source/Sequencer.h new file mode 100644 index 0000000..354e325 --- /dev/null +++ b/Apps/MidiController/main/Source/Sequencer.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#include + +// 4-track 16-step drum sequencer using esp_timer for BPM-accurate step timing. + +class Sequencer { +public: + static constexpr int NUM_TRACKS = 4; + static constexpr int NUM_STEPS = 16; + + // Called from esp_timer task when a step fires for an active track. + // onStep(step, track) - only fires for tracks that have that step enabled. + using StepCallback = std::function; + + // Called each step tick regardless of active steps - for UI playhead advance. + // Runs in esp_timer task - must be fast (store to atomic, no LVGL calls). + using TickCallback = std::function; + + Sequencer(); + ~Sequencer(); + + void start(); + void stop(); + bool isPlaying() const { return playing_.load(); } + + void setBpm(float bpm); + float getBpm() const { return bpm_.load(); } + + void toggleStep(int track, int step); + bool getStep(int track, int step) const; + void clearAll(); + + // Currently playing step (-1 if stopped). Written from esp_timer task, + // read from LVGL task - atomic for safe cross-task access. + std::atomic currentStep{-1}; + + // Callbacks - must be set before start() is called; not safe to modify + // while the sequencer is running. + StepCallback onStep; + TickCallback onTick; + +private: + bool steps_[NUM_TRACKS][NUM_STEPS] = {}; + std::atomic bpm_{120.0f}; + std::atomic playing_{false}; + std::mutex mutex_; // guards steps_ + + esp_timer_handle_t timer_ = nullptr; + + static void timerCb(void* arg); + void onTimerFired(); + void restartTimer(); +}; diff --git a/Apps/MidiController/main/Source/UiKit.h b/Apps/MidiController/main/Source/UiKit.h new file mode 100644 index 0000000..e433811 --- /dev/null +++ b/Apps/MidiController/main/Source/UiKit.h @@ -0,0 +1,159 @@ +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// MidiController UI kit +// +// Fixed-pixel instrument UI. The global LVGL theme (Material) styles every +// button/container with focus-ring margins, large paddings, pill radii and its +// own colours, so all widgets are created through these helpers, which reset +// those theme defaults back to the stock values the layouts are written for. +// +// Layout metrics are deliberately shared: every view uses the same row height, +// padding, gap and control widths so the transport/control rows look identical +// from one mode to the next. +// --------------------------------------------------------------------------- + +// --- Palette --------------------------------------------------------------- + +static inline lv_color_t MC_BG() { return lv_color_hex(0x080C12); } // view background +static inline lv_color_t MC_PANEL() { return lv_color_hex(0x0A0C14); } // row / control strip +static inline lv_color_t MC_SURFACE() { return lv_color_hex(0x1A2A40); } // raised cells / keys +static inline lv_color_t MC_BTN() { return lv_color_hex(0x2F3237); } // neutral button +static inline lv_color_t MC_BTN_ALT() { return lv_color_hex(0x222233); } // back / secondary button +static inline lv_color_t MC_LABEL() { return lv_color_hex(0x666688); } // captions +static inline lv_color_t MC_VALUE() { return lv_color_hex(0x00DFFF); } // readouts +static inline lv_color_t MC_PRIMARY() { return lv_color_hex(0x00DFFF); } // accent / highlight +static inline lv_color_t MC_OK() { return lv_color_hex(0x00AA44); } // play / start +static inline lv_color_t MC_STOP() { return lv_color_hex(0xFF2020); } // stop / recording +static inline lv_color_t MC_TEXT() { return lv_color_white(); } + +// --- Metrics --------------------------------------------------------------- + +constexpr int MC_ROW_H = 88; // transport/control row height +constexpr int MC_ROW_PAD = 12; // row inner padding +constexpr int MC_ROW_GAP = 12; // gap between row children +constexpr int MC_BTN_H = MC_ROW_H - 2 * MC_ROW_PAD; // 64 +constexpr int MC_HDR_H = 88; // header row +constexpr int MC_STATUS_H = 56; // bottom status strip + +// Control widths (shared tiers - pick the smallest that fits the row). +constexpr int MC_W_XS = 80; +constexpr int MC_W_S = 88; +constexpr int MC_W_M = 112; +constexpr int MC_W_L = 144; +constexpr int MC_W_XL = 176; +constexpr int MC_W_XXL = 224; + +// Readout-label widths. +constexpr int MC_LBL_S = 96; +constexpr int MC_LBL_M = 144; +constexpr int MC_LBL_L = 176; +constexpr int MC_LBL_XL = 224; + +// --- Widget primitives ----------------------------------------------------- + +static inline lv_obj_t* mcContainer(lv_obj_t* parent) { + lv_obj_t* obj = lv_obj_create(parent); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_radius(obj, 0, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + lv_obj_set_style_pad_gap(obj, 0, 0); + return obj; +} + +static inline lv_obj_t* mcButton(lv_obj_t* parent) { + lv_obj_t* btn = lv_button_create(parent); + // Stock dark-theme button defaults: no focus-ring margin, rounded rect, grey fill. + lv_obj_set_style_margin_all(btn, 0, 0); + lv_obj_set_style_radius(btn, 16, 0); + lv_obj_set_style_bg_color(btn, MC_BTN(), 0); + lv_obj_set_style_pad_hor(btn, 14, 0); + lv_obj_set_style_pad_ver(btn, 8, 0); + return btn; +} + +static inline lv_obj_t* mcLabel(lv_obj_t* parent, const char* txt, + const lv_font_t* font, lv_color_t color, + int width = 0) { + lv_obj_t* lbl = lv_label_create(parent); + lv_label_set_text(lbl, txt); + lv_obj_set_style_text_font(lbl, font, 0); + lv_obj_set_style_text_color(lbl, color, 0); + if (width > 0) { + lv_obj_set_width(lbl, width); + lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0); + } + return lbl; +} + +// --- Row / control builders ------------------------------------------------ + +// Panel-styled flex row with the shared height/padding/gap. +static inline lv_obj_t* mcRow(lv_obj_t* parent, int height = MC_ROW_H) { + lv_obj_t* row = mcContainer(parent); + lv_obj_set_size(row, LV_PCT(100), height); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(row, MC_PANEL(), 0); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(row, MC_ROW_PAD, 0); + lv_obj_set_style_pad_gap(row, MC_ROW_GAP, 0); + lv_obj_set_scrollable(row, false); + return row; +} + +// Small text button on a neutral fill. +static inline lv_obj_t* mcBtn(lv_obj_t* parent, const char* txt, int w, int h, + lv_event_cb_t cb, void* user) { + lv_obj_t* btn = mcButton(parent); + lv_obj_set_size(btn, w, h); + if (cb != nullptr) { + lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, user); + } + lv_obj_t* lbl = mcLabel(btn, txt, lvgl_get_text_font(FONT_SIZE_SMALL), MC_TEXT()); + lv_obj_center(lbl); + return btn; +} + +static inline lv_obj_t* mcSmallBtn(lv_obj_t* parent, const char* txt, int w, + lv_event_cb_t cb, void* user) { + return mcBtn(parent, txt, w, MC_BTN_H, cb, user); +} + +// Primary transport button ("PLAY"/"START"). +static inline lv_obj_t* mcPrimaryBtn(lv_obj_t* parent, const char* txt, int w, + lv_event_cb_t cb, void* user) { + lv_obj_t* btn = mcSmallBtn(parent, txt, w, cb, user); + lv_obj_set_style_bg_color(btn, MC_OK(), 0); + return btn; +} + +// Back / secondary navigation button. +static inline lv_obj_t* mcBackBtn(lv_obj_t* parent, lv_event_cb_t cb, void* user, + int w = MC_W_M) { + lv_obj_t* btn = mcSmallBtn(parent, "<", w, cb, user); + lv_obj_set_style_bg_color(btn, MC_BTN_ALT(), 0); + return btn; +} + +// Centered readout value. +static inline lv_obj_t* mcValue(lv_obj_t* parent, const char* txt, int w, + lv_color_t color = MC_VALUE()) { + return mcLabel(parent, txt, lvgl_get_text_font(FONT_SIZE_SMALL), color, w); +} + +// Small caption ("Rate:", "Key:", ...). +static inline lv_obj_t* mcHead(lv_obj_t* parent, const char* txt) { + return mcLabel(parent, txt, lvgl_get_text_font(FONT_SIZE_SMALL), MC_LABEL()); +} + +// Per-mode title: normal font, accent colour, grows to fill the row. +static inline lv_obj_t* mcTitle(lv_obj_t* parent, const char* txt, lv_color_t accent) { + lv_obj_t* lbl = mcLabel(parent, txt, lvgl_get_text_font(FONT_SIZE_DEFAULT), accent); + lv_obj_set_flex_grow(lbl, 1); + return lbl; +} diff --git a/Apps/MidiController/main/Source/ViewArp.cpp b/Apps/MidiController/main/Source/ViewArp.cpp new file mode 100644 index 0000000..c7cb497 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewArp.cpp @@ -0,0 +1,541 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// ARP view - Piano-triggered chord arpeggiator +// +// Layout (flex column): +// [header: < | ARP] +// [controls: Pattern | Chord | Octaves | Speed | BPM] +// [status label] +// [12-key chromatic piano row] +// --------------------------------------------------------------------------- + +static const char* ARP_PATTERNS[] = { "UP", "DOWN", "UP/DN", "RAND", "CHANCE" }; +static const char* CHORD_NAMES[] = { "MAJOR", "MINOR", "7TH" }; +// Chord intervals relative to root (in semitones) +static const int CHORD_INTERVALS[3][4] = { + { 0, 4, 7, -1 }, // MAJ + { 0, 3, 7, -1 }, // MIN + { 0, 4, 7, 10 }, // 7TH +}; +static const char* SPEED_NAMES[] = { "1/4", "1/8", "1/16", "1/32" }; +static const int SPEED_DIVS[] = { 1, 2, 4, 8 }; // subdivisions of quarter note +static const bool IS_SHARP[12] = { false,true,false,true,false,false,true,false,true,false,true,false }; + +static inline lv_coord_t dispW() { + return (lv_coord_t)lv_display_get_horizontal_resolution(nullptr); +} + +// --------------------------------------------------------------------------- +// Arpeggio timer +// --------------------------------------------------------------------------- + +static int getArpNoteCount(MidiController* mc) { + auto& ap = mc->arp_; + const int* iv = CHORD_INTERVALS[ap.chordType]; + int count = 0; + while (count < 4 && iv[count] >= 0) count++; + return count * ap.octaveRange; +} + +static int getArpNote(MidiController* mc, int step) { + auto& ap = mc->arp_; + if (ap.triggeredNote < 0) return -1; + + const int* iv = CHORD_INTERVALS[ap.chordType]; + int numIntervals = 0; + while (numIntervals < 4 && iv[numIntervals] >= 0) numIntervals++; + int total = numIntervals * ap.octaveRange; + if (total <= 0) return ap.triggeredNote; + + step = step % total; + int octOffset = step / numIntervals; + int intOffset = step % numIntervals; + int note = ap.triggeredNote + iv[intOffset] + octOffset * 12; + return std::max(0, std::min(127, note)); +} + +static void cbArpTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc || !mc->arp_.isPlaying) return; + auto& ap = mc->arp_; + + if (ap.triggeredNote < 0) return; + + int total = getArpNoteCount(mc); + if (total <= 0) return; + + // Note off for previous note + if (ap.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)ap.lastNote, 0); + ap.lastNote = -1; + } + + int note = -1; + switch (ap.pattern) { + case 0: // UP + note = getArpNote(mc, ap.arpStep); + ap.arpStep = (ap.arpStep + 1) % total; + break; + case 1: // DOWN + note = getArpNote(mc, total - 1 - ap.arpStep); + ap.arpStep = (ap.arpStep + 1) % total; + break; + case 2: // UP/DN + note = getArpNote(mc, ap.arpStep); + ap.arpStep += ap.arpDir; + if (ap.arpStep >= total) { ap.arpStep = std::max(0, total - 2); ap.arpDir = -1; } + if (ap.arpStep < 0) { ap.arpStep = std::min(1, total - 1); ap.arpDir = 1; } + break; + case 3: // RAND + note = getArpNote(mc, (int)(esp_random() % (uint32_t)total)); + break; + case 4: // CHANCE - UP with 30% skip + if ((esp_random() % 100) >= 30) { + note = getArpNote(mc, ap.arpStep); + } + ap.arpStep = (ap.arpStep + 1) % total; + break; + } + + if (note >= 0) { + mc->sendMidi(0x90, (uint8_t)note, 100); + ap.lastNote = note; + } + + // Update note display + if (ap.noteLbl && note >= 0) { + char buf[8]; + midiNoteName(note, buf, sizeof(buf)); + lv_label_set_text(ap.noteLbl, buf); + } +} + +static void updateArpTimer(MidiController* mc) { + auto& ap = mc->arp_; + if (mc->arpTimer_) { + // Interval: quarter note at bpm / subdivision factor + uint32_t intervalMs = (uint32_t)(60000.0f / ap.bpm / SPEED_DIVS[ap.speedIdx]); + if (intervalMs < 20) intervalMs = 20; + lv_timer_set_period(mc->arpTimer_, intervalMs); + } +} + +static void startArp(MidiController* mc, int rootNote) { + auto& ap = mc->arp_; + ap.triggeredNote = rootNote; + ap.arpStep = 0; + ap.arpDir = 1; + ap.isPlaying = true; + + uint32_t intervalMs = (uint32_t)(60000.0f / ap.bpm / SPEED_DIVS[ap.speedIdx]); + if (intervalMs < 20) intervalMs = 20; + + if (!mc->arpTimer_) { + mc->arpTimer_ = lv_timer_create(cbArpTimer, intervalMs, mc); + } else { + lv_timer_set_period(mc->arpTimer_, intervalMs); + lv_timer_reset(mc->arpTimer_); + } + + if (ap.statusLbl) { + char buf[24]; + char noteName[8]; + midiNoteName(rootNote, noteName, sizeof(noteName)); + snprintf(buf, sizeof(buf), "Arping: %s %s", noteName, CHORD_NAMES[ap.chordType]); + lv_label_set_text(ap.statusLbl, buf); + } +} + +static void stopArp(MidiController* mc) { + auto& ap = mc->arp_; + ap.isPlaying = false; + ap.triggeredNote = -1; + if (ap.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)ap.lastNote, 0); + ap.lastNote = -1; + } + if (ap.noteLbl) lv_label_set_text(ap.noteLbl, ""); + if (ap.statusLbl) lv_label_set_text(ap.statusLbl, "Tap a key to start"); +} + +// --------------------------------------------------------------------------- +// Control callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbPatternPrev(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->arp_.pattern = (mc->arp_.pattern + 4) % 5; + if (mc->arp_.patternLbl) lv_label_set_text(mc->arp_.patternLbl, ARP_PATTERNS[mc->arp_.pattern]); +} +static void cbPatternNext(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->arp_.pattern = (mc->arp_.pattern + 1) % 5; + if (mc->arp_.patternLbl) lv_label_set_text(mc->arp_.patternLbl, ARP_PATTERNS[mc->arp_.pattern]); +} + +static void cbChordCycle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->arp_.chordType = (mc->arp_.chordType + 1) % 3; + if (mc->arp_.chordLbl) lv_label_set_text(mc->arp_.chordLbl, CHORD_NAMES[mc->arp_.chordType]); +} + +static void cbOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->arp_.octaveRange > 1) mc->arp_.octaveRange--; + if (mc->arp_.octLbl) { + char buf[4]; snprintf(buf, sizeof(buf), "%d", mc->arp_.octaveRange); + lv_label_set_text(mc->arp_.octLbl, buf); + } +} +static void cbOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->arp_.octaveRange < 4) mc->arp_.octaveRange++; + if (mc->arp_.octLbl) { + char buf[4]; snprintf(buf, sizeof(buf), "%d", mc->arp_.octaveRange); + lv_label_set_text(mc->arp_.octLbl, buf); + } +} + +static void cbSpeedDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->arp_.speedIdx > 0) mc->arp_.speedIdx--; + if (mc->arp_.speedLbl) lv_label_set_text(mc->arp_.speedLbl, SPEED_NAMES[mc->arp_.speedIdx]); + updateArpTimer(mc); +} +static void cbSpeedUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->arp_.speedIdx < 3) mc->arp_.speedIdx++; + if (mc->arp_.speedLbl) lv_label_set_text(mc->arp_.speedLbl, SPEED_NAMES[mc->arp_.speedIdx]); + updateArpTimer(mc); +} + +static void cbBpmDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->arp_.bpm -= 5; if (mc->arp_.bpm < 40) mc->arp_.bpm = 40; + if (mc->arp_.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", mc->arp_.bpm); + lv_label_set_text(mc->arp_.bpmLbl, buf); + } + updateArpTimer(mc); +} +static void cbBpmUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->arp_.bpm += 5; if (mc->arp_.bpm > 300) mc->arp_.bpm = 300; + if (mc->arp_.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", mc->arp_.bpm); + lv_label_set_text(mc->arp_.bpmLbl, buf); + } + updateArpTimer(mc); +} + +static void updatePianoOctLabel(MidiController* mc) { + if (mc->arp_.pianoOctLbl) { + char buf[4]; snprintf(buf, sizeof(buf), "%d", mc->arp_.pianoOctave); + lv_label_set_text(mc->arp_.pianoOctLbl, buf); + } +} + +static void rebuildPianoKeyNotes(MidiController* mc) { + auto& ap = mc->arp_; + // Reset all key highlights before re-assigning notes (octave change invalidates old highlight) + for (int k = 0; k < 12; k++) { + if (!ap.pianoKey[k]) continue; + bool isSharp = IS_SHARP[k]; + lv_obj_set_style_bg_color(ap.pianoKey[k], + isSharp ? lv_color_hex(0xDDDDCC) : lv_color_hex(0x111111), + LV_PART_MAIN); + } + ap.triggeredNote = -1; // old note no longer valid in new octave + stopArp(mc); + + for (int k = 0; k < 12; k++) { + if (!ap.pianoKey[k]) continue; + int note = std::max(0, std::min(127, (ap.pianoOctave + 1) * 12 + k)); + lv_obj_set_user_data(ap.pianoKey[k], (void*)(intptr_t)note); + // Update label (child 0) + lv_obj_t* lbl = lv_obj_get_child(ap.pianoKey[k], 0); + if (lbl) { + char noteName[8]; midiNoteName(note, noteName, sizeof(noteName)); + lv_label_set_text(lbl, noteName); + } + } + updatePianoOctLabel(mc); +} + +static void cbPianoOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->arp_.pianoOctave > 1) mc->arp_.pianoOctave--; + rebuildPianoKeyNotes(mc); +} +static void cbPianoOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->arp_.pianoOctave < 7) mc->arp_.pianoOctave++; + rebuildPianoKeyNotes(mc); +} + +static void cbPianoKey(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ap = mc->arp_; + + lv_obj_t* btn = lv_event_get_target_obj(e); + int note = (int)(intptr_t)lv_obj_get_user_data(btn); + + // isSharp: sharps (C#,D#,F#,G#,A#) are light keys + auto keyColor = [](int semitone) -> uint32_t { + return IS_SHARP[semitone] ? 0xDDDDCC : 0x111111; + }; + + if (ap.triggeredNote == note && ap.isPlaying) { + // Tap same key to stop + stopArp(mc); + lv_obj_set_style_bg_color(btn, lv_color_hex(keyColor(note % 12)), LV_PART_MAIN); + } else { + // Reset previously highlighted key + if (ap.triggeredNote >= 0) { + int prevKey = ap.triggeredNote % 12; + if (ap.pianoKey[prevKey]) { + lv_obj_set_style_bg_color(ap.pianoKey[prevKey], + lv_color_hex(keyColor(prevKey)), + LV_PART_MAIN); + } + } + startArp(mc, note); + lv_obj_set_style_bg_color(btn, lv_color_hex(0x00DFFF), LV_PART_MAIN); + } +} + +// --------------------------------------------------------------------------- +// Teardown +// --------------------------------------------------------------------------- + +static void cbArpRootDelete(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ap = mc->arp_; + + // Stop and delete the arp timer so it cannot fire after the widgets are gone + if (mc->arpTimer_) { + lv_timer_delete(mc->arpTimer_); + mc->arpTimer_ = nullptr; + } + stopArp(mc); + + // Null all stored LVGL pointers - objects are being destroyed + ap.noteLbl = nullptr; + ap.statusLbl = nullptr; + ap.pianoOctLbl = nullptr; + for (int k = 0; k < 12; k++) ap.pianoKey[k] = nullptr; +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildArpView(MidiController* mc, lv_obj_t* parent) { + auto& ap = mc->arp_; + + lv_coord_t W = dispW(); + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + + int hdrH = MC_HDR_H; + int ctrlH = 88; + int statusH = 56; + + lv_obj_t* root = mcContainer(parent); + lv_obj_add_event_cb(root, cbArpRootDelete, LV_EVENT_DELETE, mc); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 1, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // Helper lambda to create a control row + auto makeRow = [&](int h) -> lv_obj_t* { return mcRow(root, h); }; + + auto makeSmallBtn = [&](lv_obj_t* parent_, const char* txt, lv_event_cb_t cb) -> lv_obj_t* { + return mcSmallBtn(parent_, txt, MC_W_S, cb, mc); + }; + + auto makeLbl = [&](lv_obj_t* parent_, const char* txt, int w, lv_color_t col) -> lv_obj_t* { + return mcValue(parent_, txt, w, col); + }; + + // -- Header row ----------------------------------------------------------- + lv_obj_t* hdr = makeRow(hdrH); + mcBackBtn(hdr, cbBack, mc, MC_W_M); + + lv_obj_t* titleLbl = lv_label_create(hdr); + lv_label_set_text(titleLbl, "ARPEGGIATOR"); + lv_obj_set_style_text_font(titleLbl, fontNormal, 0); + lv_obj_set_style_text_color(titleLbl, lv_color_hex(0xFF8000), 0); + lv_obj_set_flex_grow(titleLbl, 1); + + // -- Pattern row --------------------------------------------------------- + lv_obj_t* patRow = makeRow(ctrlH); + lv_obj_t* patLblHead = lv_label_create(patRow); + lv_label_set_text(patLblHead, "PTRN:"); + lv_obj_set_style_text_font(patLblHead, fontSmall, 0); + lv_obj_set_style_text_color(patLblHead, lv_color_hex(0x666688), 0); + makeSmallBtn(patRow, "<", cbPatternPrev); + ap.patternLbl = makeLbl(patRow, ARP_PATTERNS[ap.pattern], 144, lv_color_hex(0x00DFFF)); + makeSmallBtn(patRow, ">", cbPatternNext); + + lv_obj_t* sep1 = lv_label_create(patRow); + lv_label_set_text(sep1, " CHORD:"); + lv_obj_set_style_text_font(sep1, fontSmall, 0); + lv_obj_set_style_text_color(sep1, lv_color_hex(0x666688), 0); + lv_obj_t* chordBtn = mcButton(patRow); + lv_obj_set_size(chordBtn, 144, MC_BTN_H); + lv_obj_add_event_cb(chordBtn, cbChordCycle, LV_EVENT_CLICKED, mc); + ap.chordLbl = lv_label_create(chordBtn); + lv_label_set_text(ap.chordLbl, CHORD_NAMES[ap.chordType]); + lv_obj_set_style_text_font(ap.chordLbl, fontSmall, 0); + lv_obj_center(ap.chordLbl); + + // -- Octave / Speed / BPM row --------------------------------------------- + lv_obj_t* paramRow = makeRow(ctrlH); + lv_obj_t* octHead = lv_label_create(paramRow); + lv_label_set_text(octHead, "OCT:"); + lv_obj_set_style_text_font(octHead, fontSmall, 0); + lv_obj_set_style_text_color(octHead, lv_color_hex(0x666688), 0); + makeSmallBtn(paramRow, "-", cbOctDown); + char octBuf[4]; snprintf(octBuf, sizeof(octBuf), "%d", ap.octaveRange); + ap.octLbl = makeLbl(paramRow, octBuf, 80, lv_color_hex(0xFFFFFF)); + makeSmallBtn(paramRow, "+", cbOctUp); + + lv_obj_t* spdHead = lv_label_create(paramRow); + lv_label_set_text(spdHead, " SPD:"); + lv_obj_set_style_text_font(spdHead, fontSmall, 0); + lv_obj_set_style_text_color(spdHead, lv_color_hex(0x666688), 0); + makeSmallBtn(paramRow, "<", cbSpeedDown); + ap.speedLbl = makeLbl(paramRow, SPEED_NAMES[ap.speedIdx], 144, lv_color_hex(0x00DFFF)); + makeSmallBtn(paramRow, ">", cbSpeedUp); + + lv_obj_t* bpmHead = lv_label_create(paramRow); + lv_label_set_text(bpmHead, " BPM:"); + lv_obj_set_style_text_font(bpmHead, fontSmall, 0); + lv_obj_set_style_text_color(bpmHead, lv_color_hex(0x666688), 0); + makeSmallBtn(paramRow, "-", cbBpmDown); + char bpmBuf[8]; snprintf(bpmBuf, sizeof(bpmBuf), "%d", ap.bpm); + ap.bpmLbl = makeLbl(paramRow, bpmBuf, 160, lv_color_hex(0xFFE000)); + makeSmallBtn(paramRow, "+", cbBpmUp); + + // -- Status + note label row ----------------------------------------------- + lv_obj_t* statusRow = mcContainer(root); + lv_obj_set_size(statusRow, LV_PCT(100), statusH); + lv_obj_set_flex_flow(statusRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(statusRow, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(statusRow, lv_color_hex(0x0A0C14), 0); + lv_obj_set_style_bg_opa(statusRow, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(statusRow, 0, 0); + lv_obj_set_style_pad_all(statusRow, 8, 0); + lv_obj_set_scrollable(statusRow, false); + + ap.statusLbl = lv_label_create(statusRow); + lv_label_set_text(ap.statusLbl, "Tap a key to start"); + lv_obj_set_style_text_font(ap.statusLbl, fontSmall, 0); + lv_obj_set_style_text_color(ap.statusLbl, lv_color_hex(0x666688), 0); + + ap.noteLbl = lv_label_create(statusRow); + lv_label_set_text(ap.noteLbl, ""); + lv_obj_set_style_text_font(ap.noteLbl, fontSmall, 0); + lv_obj_set_style_text_color(ap.noteLbl, lv_color_hex(0x00DFFF), 0); + + // -- Piano octave controls + piano row -------------------------------------- + lv_obj_t* pianoHdrRow = makeRow(72); + lv_obj_t* pOctHead = lv_label_create(pianoHdrRow); + lv_label_set_text(pOctHead, "Piano Oct:"); + lv_obj_set_style_text_font(pOctHead, fontSmall, 0); + lv_obj_set_style_text_color(pOctHead, lv_color_hex(0x666688), 0); + lv_obj_t* pOctDown = mcButton(pianoHdrRow); + lv_obj_set_size(pOctDown, 88, 56); + lv_obj_add_event_cb(pOctDown, cbPianoOctDown, LV_EVENT_CLICKED, mc); + lv_obj_t* podl = lv_label_create(pOctDown); lv_label_set_text(podl, "-"); + lv_obj_set_style_text_font(podl, fontSmall, 0); lv_obj_center(podl); + char pianoOctBuf[4]; snprintf(pianoOctBuf, sizeof(pianoOctBuf), "%d", ap.pianoOctave); + ap.pianoOctLbl = lv_label_create(pianoHdrRow); + lv_label_set_text(ap.pianoOctLbl, pianoOctBuf); + lv_obj_set_style_text_font(ap.pianoOctLbl, fontSmall, 0); + lv_obj_set_style_text_color(ap.pianoOctLbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_width(ap.pianoOctLbl, 72); + lv_obj_set_style_text_align(ap.pianoOctLbl, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_t* pOctUp = mcButton(pianoHdrRow); + lv_obj_set_size(pOctUp, 88, 56); + lv_obj_add_event_cb(pOctUp, cbPianoOctUp, LV_EVENT_CLICKED, mc); + lv_obj_t* poul = lv_label_create(pOctUp); lv_label_set_text(poul, "+"); + lv_obj_set_style_text_font(poul, fontSmall, 0); lv_obj_center(poul); + + // Piano row - flex_grow fills the rest + lv_obj_t* pianoArea = mcContainer(root); + lv_obj_set_width(pianoArea, LV_PCT(100)); + lv_obj_set_flex_grow(pianoArea, 1); + lv_obj_set_style_bg_color(pianoArea, lv_color_hex(0x050508), 0); + lv_obj_set_style_bg_opa(pianoArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(pianoArea, 0, 0); + lv_obj_set_style_pad_all(pianoArea, 0, 0); + lv_obj_set_scrollable(pianoArea, false); + + // 12 chromatic keys - all same height, equal width. + // Original style: naturals (C,D,E,F,G,A,B) = dark keys, sharps = light keys with labels. + int keyW = (int)W / 12; + + for (int k = 0; k < 12; k++) { + int note = (ap.pianoOctave + 1) * 12 + k; + note = std::max(0, std::min(127, note)); + + lv_obj_t* btn = mcButton(pianoArea); + lv_obj_set_pos(btn, k * keyW, 0); + bool isSharp = IS_SHARP[k]; + lv_obj_set_size(btn, keyW - 1, LV_PCT(100)); + // Sharps = light (cream), naturals = dark + lv_obj_set_style_bg_color(btn, isSharp ? lv_color_hex(0xDDDDCC) : lv_color_hex(0x111111), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(btn, lv_color_hex(0x444444), 0); + lv_obj_set_style_border_width(btn, 1, 0); + lv_obj_set_style_radius(btn, 2, 0); + lv_obj_set_style_pad_all(btn, 0, 0); + lv_obj_set_user_data(btn, (void*)(intptr_t)note); + lv_obj_add_event_cb(btn, cbPianoKey, LV_EVENT_CLICKED, mc); + + // Note name label on all keys + char noteName[8]; + midiNoteName(note, noteName, sizeof(noteName)); + lv_obj_t* lbl = lv_label_create(btn); + lv_label_set_text(lbl, noteName); + lv_obj_set_style_text_font(lbl, fontSmall, 0); + lv_obj_set_style_text_color(lbl, isSharp ? lv_color_hex(0x333333) : lv_color_hex(0x888888), 0); + lv_obj_align(lbl, LV_ALIGN_BOTTOM_MID, 0, -2); + lv_obj_set_clickable(lbl, false); + + ap.pianoKey[k] = btn; + } +} diff --git a/Apps/MidiController/main/Source/ViewBaby8.cpp b/Apps/MidiController/main/Source/ViewBaby8.cpp new file mode 100644 index 0000000..4cc4017 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewBaby8.cpp @@ -0,0 +1,417 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// BABY8 view - 8-step melodic sequencer with swing +// +// Layout (flex column): +// [ctrl row: < | PLAY | BPM- | N | BPM+ | SWING- | N% | SWING+] +// [step grid row (flex_grow): 8 step cells] +// [edit row: NOTE- | noteName | NOTE+ VEL- | vel | VEL+ GATE PAT- | P:N | PAT+] +// --------------------------------------------------------------------------- + + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +void baby8UpdateStepColors(MidiController* mc) { + auto& bs = mc->baby8_; + for (int i = 0; i < 8; i++) { + if (!bs.stepBtn[i]) continue; + const auto& s = bs.steps[i]; + uint32_t col; + if (i == bs.currentStep && bs.playing) { + col = s.gate ? 0xFFFFFF : 0x4488FF; + } else if (i == bs.selectedStep) { + col = s.gate ? 0xFF8800 : 0x886633; + } else { + col = s.gate ? 0x006622 : 0x222222; + } + lv_obj_set_style_bg_color(bs.stepBtn[i], lv_color_hex(col), 0); + // Update note label + if (bs.noteLbl[i] && s.gate) { + char buf[8]; midiNoteName(s.note, buf, sizeof(buf)); + lv_label_set_text(bs.noteLbl[i], buf); + } else if (bs.noteLbl[i]) { + lv_label_set_text(bs.noteLbl[i], "--"); + } + } +} + +static void baby8UpdateEditRow(MidiController* mc) { + auto& bs = mc->baby8_; + const auto& s = bs.steps[bs.selectedStep]; + if (bs.editNoteLbl) { + char buf[8]; midiNoteName(s.note, buf, sizeof(buf)); + lv_label_set_text(bs.editNoteLbl, buf); + } + if (bs.editVelLbl) { + char buf[6]; snprintf(buf, sizeof(buf), "%d", s.velocity); + lv_label_set_text(bs.editVelLbl, buf); + } +} + +// --------------------------------------------------------------------------- +// Timer callback +// --------------------------------------------------------------------------- + +static void cbBaby8Timer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + auto& bs = mc->baby8_; + if (!bs.playing) return; + + uint32_t now = lv_tick_get(); + + // Note-off + if (bs.lastNote >= 0 && bs.nextStepMs >= 10 && now >= bs.nextStepMs - 10) { + mc->sendMidi(0x80, (uint8_t)bs.lastNote, 0); + bs.lastNote = -1; + } + + if (now < bs.nextStepMs) return; + + // Advance step + bs.currentStep = (bs.currentStep + 1) % 8; + bool even = (bs.currentStep % 2 == 0); + + // BPM → 16th note duration + uint32_t beatMs = 60000u / (uint32_t)bs.bpm; + uint32_t sixteenth = beatMs / 4; + + // Swing: lengthen even steps and shorten odd steps by the same amount so a + // pair still fills two 16th notes and the overall tempo does not drift. + // swing=50 → no swing (both equal); swing>50 → even step longer. + uint32_t stepDur = sixteenth; + if (bs.swing > 50) { + if (even) stepDur = (uint32_t)(sixteenth * bs.swing / 50u); + else stepDur = (uint32_t)(sixteenth * (100u - bs.swing) / 50u); + if (stepDur < 20) stepDur = 20; + } + + bs.nextStepMs = now + stepDur; + + // Play current step + const auto& step = bs.steps[bs.currentStep]; + if (step.gate) { + mc->sendMidi(0x90, (uint8_t)step.note, (uint8_t)step.velocity); + bs.lastNote = step.note; + } + + // Update step highlight + baby8UpdateStepColors(mc); +} + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbBaby8Back(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbBaby8Play(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + bs.playing = !bs.playing; + if (bs.playing) { + bs.currentStep = 7; // will advance to 0 on first tick + uint32_t beatMs = 60000u / (uint32_t)bs.bpm; + bs.nextStepMs = lv_tick_get() + beatMs / 4; + } else { + if (bs.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)bs.lastNote, 0); + bs.lastNote = -1; + } + baby8UpdateStepColors(mc); + } + if (bs.playLbl) lv_label_set_text(bs.playLbl, bs.playing ? "STOP" : "PLAY"); +} + +static void cbBaby8BpmDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + if (bs.bpm > 40) bs.bpm -= 5; + if (bs.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", bs.bpm); + lv_label_set_text(bs.bpmLbl, buf); + } +} +static void cbBaby8BpmUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + if (bs.bpm < 280) bs.bpm += 5; + if (bs.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", bs.bpm); + lv_label_set_text(bs.bpmLbl, buf); + } +} + +static void cbBaby8SwingDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + if (bs.swing > 50) bs.swing -= 5; + if (bs.swingLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d%%", bs.swing); + lv_label_set_text(bs.swingLbl, buf); + } +} +static void cbBaby8SwingUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + if (bs.swing < 80) bs.swing += 5; + if (bs.swingLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d%%", bs.swing); + lv_label_set_text(bs.swingLbl, buf); + } +} + +static void cbBaby8StepTap(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + // Find which step button was tapped by matching lv_obj + lv_obj_t* obj = lv_event_get_target_obj(e); + for (int i = 0; i < 8; i++) { + if (bs.stepBtn[i] == obj) { + bs.selectedStep = i; + break; + } + } + baby8UpdateStepColors(mc); + baby8UpdateEditRow(mc); +} + +static void cbBaby8NoteDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + auto& s = bs.steps[bs.selectedStep]; + if (s.note > 0) s.note--; + baby8UpdateEditRow(mc); + baby8UpdateStepColors(mc); +} +static void cbBaby8NoteUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + auto& s = bs.steps[bs.selectedStep]; + if (s.note < 127) s.note++; + baby8UpdateEditRow(mc); + baby8UpdateStepColors(mc); +} + +static void cbBaby8VelDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + auto& s = bs.steps[bs.selectedStep]; + if (s.velocity > 10) s.velocity -= 10; + else s.velocity = 1; + baby8UpdateEditRow(mc); +} +static void cbBaby8VelUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + auto& s = bs.steps[bs.selectedStep]; + if (s.velocity <= 117) s.velocity += 10; + else s.velocity = 127; + baby8UpdateEditRow(mc); +} + +static void cbBaby8Gate(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + bs.steps[bs.selectedStep].gate = !bs.steps[bs.selectedStep].gate; + baby8UpdateEditRow(mc); + baby8UpdateStepColors(mc); +} + +static void cbBaby8PatDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + // Save current pattern before switching + memcpy(bs.patterns[bs.patternIdx], bs.steps, sizeof(bs.steps)); + bs.patternIdx = (bs.patternIdx + 7) % 8; + memcpy(bs.steps, bs.patterns[bs.patternIdx], sizeof(bs.steps)); + if (bs.patLbl) { + char buf[6]; snprintf(buf, sizeof(buf), "P:%d", bs.patternIdx + 1); + lv_label_set_text(bs.patLbl, buf); + } + baby8UpdateStepColors(mc); + baby8UpdateEditRow(mc); +} +static void cbBaby8PatUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& bs = mc->baby8_; + // Save current pattern before switching + memcpy(bs.patterns[bs.patternIdx], bs.steps, sizeof(bs.steps)); + bs.patternIdx = (bs.patternIdx + 1) % 8; + memcpy(bs.steps, bs.patterns[bs.patternIdx], sizeof(bs.steps)); + if (bs.patLbl) { + char buf[6]; snprintf(buf, sizeof(buf), "P:%d", bs.patternIdx + 1); + lv_label_set_text(bs.patLbl, buf); + } + baby8UpdateStepColors(mc); + baby8UpdateEditRow(mc); +} + +// --------------------------------------------------------------------------- +// Build +// --------------------------------------------------------------------------- + +void buildBaby8View(MidiController* mc, lv_obj_t* parent) { + auto& bs = mc->baby8_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + + int sBtnW = 88; + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x070A10), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 8, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + auto makeRow = [&]() -> lv_obj_t* { return mcRow(root); }; + + auto makeBtn = [&](lv_obj_t* par, const char* txt, int w, lv_event_cb_t cb, void* ud) -> lv_obj_t* { + return mcSmallBtn(par, txt, w, cb, ud); + }; + + auto makeLbl = [&](lv_obj_t* par, const char* txt, int w, lv_color_t col) -> lv_obj_t* { + return mcValue(par, txt, w, col); + }; + + auto makeSep = [&](lv_obj_t* par) { + lv_obj_t* sep = lv_label_create(par); + lv_label_set_text(sep, "|"); + lv_obj_set_style_text_font(sep, fontSmall, 0); + lv_obj_set_style_text_color(sep, lv_color_hex(0x334455), 0); + }; + + // -- Control row ----------------------------------------------------------- + { + lv_obj_t* row = makeRow(); + mcBackBtn(row, cbBaby8Back, mc, MC_W_S); + lv_obj_t* titleLbl = lv_label_create(row); + lv_label_set_text(titleLbl, "BABY-8"); + lv_obj_set_style_text_font(titleLbl, fontNormal, 0); + lv_obj_set_style_text_color(titleLbl, lv_color_hex(0x44BBFF), 0); + lv_obj_set_flex_grow(titleLbl, 1); + lv_obj_t* playBtn = makeBtn(row, bs.playing ? "STOP" : "PLAY", 176, cbBaby8Play, mc); + bs.playLbl = lv_obj_get_child(playBtn, 0); + makeSep(row); + makeBtn(row, "<", sBtnW, cbBaby8BpmDown, mc); + char bufBpm[8]; snprintf(bufBpm, sizeof(bufBpm), "%d", bs.bpm); + bs.bpmLbl = makeLbl(row, bufBpm, 136, lv_color_hex(0xFFE000)); + makeBtn(row, ">", sBtnW, cbBaby8BpmUp, mc); + makeSep(row); + makeBtn(row, "<", sBtnW, cbBaby8SwingDown, mc); + char bufSw[8]; snprintf(bufSw, sizeof(bufSw), "%d%%", bs.swing); + bs.swingLbl = makeLbl(row, bufSw, 128, lv_color_hex(0xAADDFF)); + makeBtn(row, ">", sBtnW, cbBaby8SwingUp, mc); + } + + // -- Step grid (flex_grow) ----------------------------------------------------------- + { + lv_obj_t* gridArea = mcContainer(root); + lv_obj_set_width(gridArea, LV_PCT(100)); + lv_obj_set_flex_grow(gridArea, 1); + lv_obj_set_flex_flow(gridArea, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(gridArea, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(gridArea, lv_color_hex(0x050810), 0); + lv_obj_set_style_bg_opa(gridArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(gridArea, 0, 0); + lv_obj_set_style_pad_all(gridArea, 16, 0); + lv_obj_set_style_pad_gap(gridArea, 12, 0); + lv_obj_set_scrollable(gridArea, false); + + // Compute cell size from available space + int cellW = (lv_display_get_horizontal_resolution(nullptr) - 32 - 7 * 12) / 8; + + for (int i = 0; i < 8; i++) { + lv_obj_t* cell = mcContainer(gridArea); + lv_obj_set_size(cell, cellW, LV_PCT(100)); + lv_obj_set_flex_flow(cell, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(cell, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_radius(cell, 16, 0); + lv_obj_set_style_border_width(cell, 1, 0); + lv_obj_set_style_border_color(cell, lv_color_hex(0x334455), 0); + lv_obj_set_style_bg_color(cell, lv_color_hex(bs.steps[i].gate ? 0x006622u : 0x222222u), 0); + lv_obj_set_style_bg_opa(cell, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(cell, 8, 0); + lv_obj_set_style_pad_gap(cell, 8, 0); + lv_obj_set_scrollable(cell, false); + lv_obj_set_clickable(cell, true); + lv_obj_add_event_cb(cell, cbBaby8StepTap, LV_EVENT_CLICKED, mc); + bs.stepBtn[i] = cell; + + // Step number + lv_obj_t* numLbl = lv_label_create(cell); + char nbuf[4]; snprintf(nbuf, sizeof(nbuf), "%d", i + 1); + lv_label_set_text(numLbl, nbuf); + lv_obj_set_style_text_font(numLbl, fontSmall, 0); + lv_obj_set_style_text_color(numLbl, lv_color_hex(0x888888), 0); + + // Note name + lv_obj_t* noteLbl = lv_label_create(cell); + char noteBuf[8]; midiNoteName(bs.steps[i].note, noteBuf, sizeof(noteBuf)); + lv_label_set_text(noteLbl, bs.steps[i].gate ? noteBuf : "--"); + lv_obj_set_style_text_font(noteLbl, fontSmall, 0); + lv_obj_set_style_text_color(noteLbl, lv_color_hex(0xFFFFFF), 0); + bs.noteLbl[i] = noteLbl; + } + } + + // -- Edit row ----------------------------------------------------------- + { + lv_obj_t* row = makeRow(); + // Note + makeBtn(row, "<", sBtnW, cbBaby8NoteDown, mc); + char noteBuf[8]; midiNoteName(bs.steps[bs.selectedStep].note, noteBuf, sizeof(noteBuf)); + bs.editNoteLbl = makeLbl(row, noteBuf, 128, lv_color_hex(0x88DDFF)); + makeBtn(row, ">", sBtnW, cbBaby8NoteUp, mc); + // Velocity + makeBtn(row, "<", sBtnW, cbBaby8VelDown, mc); + char velBuf[6]; snprintf(velBuf, sizeof(velBuf), "%d", bs.steps[bs.selectedStep].velocity); + bs.editVelLbl = makeLbl(row, velBuf, 112, lv_color_hex(0xFFCC88)); + makeBtn(row, ">", sBtnW, cbBaby8VelUp, mc); + // Gate + makeBtn(row, "GATE", 160, cbBaby8Gate, mc); + // Pattern + makeBtn(row, "<", sBtnW, cbBaby8PatDown, mc); + char patBuf[16]; snprintf(patBuf, sizeof(patBuf), "P:%d", bs.patternIdx + 1); + bs.patLbl = makeLbl(row, patBuf, 112, lv_color_hex(0xAA88FF)); + makeBtn(row, ">", sBtnW, cbBaby8PatUp, mc); + } + + // -- Start timer ----------------------------------------------------------- + bs.nextStepMs = lv_tick_get() + 60000u / (uint32_t)bs.bpm / 4u; + if (mc->baby8Timer_) { + lv_timer_delete(mc->baby8Timer_); + } + mc->baby8Timer_ = lv_timer_create(cbBaby8Timer, 10, mc); +} diff --git a/Apps/MidiController/main/Source/ViewBeats.cpp b/Apps/MidiController/main/Source/ViewBeats.cpp new file mode 100644 index 0000000..de41618 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewBeats.cpp @@ -0,0 +1,347 @@ +#include "MidiController.h" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// BEATS view - 4-track 16-step drum sequencer +// +// Layout (flex column): +// [transport row: BACK | PLAY/STOP | CLR | BPM- | BPM | BPM+ | "BPM"] +// [step-marker header row (beat numbers 1/5/9/13)] +// [4 × 16 step button grid - flex_grow, each row = track label + 16 buttons] +// --------------------------------------------------------------------------- + +// Drum track names and colors +static const char* TRACK_NAMES[] = { "KICK", "SNRE", "HHAT", "OPEN" }; + +static lv_color_t trackColor(int t) { + switch (t) { + case 0: return lv_color_hex(0xFF2020); // red + case 1: return lv_color_hex(0xFFE000); // yellow + case 2: return lv_color_hex(0x00DFFF); // cyan + case 3: return lv_color_hex(0xCC44FF); // purple + default: return lv_color_hex(0x888888); + } +} + +static inline lv_coord_t dispW() { + return (lv_coord_t)lv_display_get_horizontal_resolution(nullptr); +} + +// Beat group separator: every 4 steps gets a slightly different shade +static bool isBeatBoundary(int step) { return (step % 4) == 0; } + +static void refreshStepBtn(MidiController* mc, int track, int step) { + lv_obj_t* btn = mc->beats_.stepBtn[track][step]; + if (!btn) return; + bool active = mc->seq_ && mc->seq_->getStep(track, step); + lv_obj_set_style_bg_color(btn, + active ? trackColor(track) : lv_color_hex(0x1A2A40), + LV_PART_MAIN); + // Restore beat-boundary left border (overwritten by playback cursor highlight) + if (isBeatBoundary(step)) { + lv_obj_set_style_border_width(btn, 1, LV_PART_MAIN); + lv_obj_set_style_border_side(btn, LV_BORDER_SIDE_LEFT, LV_PART_MAIN); + lv_obj_set_style_border_color(btn, lv_color_hex(0x334455), LV_PART_MAIN); + } else { + lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN); + } +} + +static void refreshAllStepBtns(MidiController* mc) { + for (int t = 0; t < Sequencer::NUM_TRACKS; t++) { + for (int s = 0; s < Sequencer::NUM_STEPS; s++) { + refreshStepBtn(mc, t, s); + } + } +} + +static void refreshPlayBtn(MidiController* mc) { + if (!mc->beats_.playLbl) return; + lv_label_set_text(mc->beats_.playLbl, + mc->beats_.playing ? "STOP" : "PLAY"); + lv_obj_set_style_bg_color(mc->beats_.playBtn, + mc->beats_.playing ? lv_color_hex(0xFF2020) : lv_color_hex(0x00AA44), + LV_PART_MAIN); +} + +static void refreshBpmLbl(MidiController* mc) { + if (!mc->beats_.bpmLbl) return; + char buf[8]; snprintf(buf, sizeof(buf), "%d", mc->beats_.bpm); + lv_label_set_text(mc->beats_.bpmLbl, buf); +} + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbStepBtn(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc || !mc->seq_) return; + lv_obj_t* btn = lv_event_get_target_obj(e); + int packed = (int)(intptr_t)lv_obj_get_user_data(btn); + int track = packed >> 8; + int step = packed & 0xFF; + mc->seq_->toggleStep(track, step); + refreshStepBtn(mc, track, step); +} + +static void cbPlayStop(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc || !mc->seq_) return; + if (mc->beats_.playing) { + mc->seq_->stop(); + mc->beats_.playing = false; + mc->stopAllNotes(); + mc->seqPlayStep.store(-1); + mc->seqLastStep = -1; + refreshAllStepBtns(mc); // self-heals borders and colors + } else { + mc->seqLastStep = -1; + mc->seq_->start(); + mc->beats_.playing = true; + } + refreshPlayBtn(mc); +} + +static void cbClear(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc || !mc->seq_) return; + mc->seq_->clearAll(); + // clearAll() also stops the sequencer, so bring the transport UI back in sync. + mc->beats_.playing = false; + mc->seqPlayStep.store(-1); + mc->seqLastStep = -1; + mc->stopAllNotes(); + refreshPlayBtn(mc); + refreshAllStepBtns(mc); +} + +static void cbBpmDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->beats_.bpm -= 5; + if (mc->beats_.bpm < 40) mc->beats_.bpm = 40; + if (mc->seq_) mc->seq_->setBpm((float)mc->beats_.bpm); + refreshBpmLbl(mc); +} + +static void cbBpmUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->beats_.bpm += 5; + if (mc->beats_.bpm > 300) mc->beats_.bpm = 300; + if (mc->seq_) mc->seq_->setBpm((float)mc->beats_.bpm); + refreshBpmLbl(mc); +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildBeatsView(MidiController* mc, lv_obj_t* parent) { + auto& bs = mc->beats_; + if (mc->seq_) mc->seq_->setBpm((float)bs.bpm); + + lv_coord_t W = dispW(); + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + + int labelW = 152; // track label column width + int ctrlH = 88; // transport row height (matches KEYS) + int btnH = MC_BTN_H; + + // Available width for step grid (excluding track label and row padding) + lv_coord_t gridW = W - labelW - 8 * 2; // row has pad_all=4 each side + // Step button width: account for 15 gaps between 16 steps (pad_gap=4) + int stepW = (gridW - 4 * 15) / Sequencer::NUM_STEPS; + + // Root + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // -- Transport row (TOP) -------------------------------- + lv_obj_t* transport = mcRow(root, ctrlH); + + // Back button + mcBackBtn(transport, cbBack, mc, MC_W_S); + + lv_obj_t* titleLbl = lv_label_create(transport); + lv_label_set_text(titleLbl, "BEATS"); + lv_obj_set_style_text_font(titleLbl, fontNormal, 0); + lv_obj_set_style_text_color(titleLbl, lv_color_hex(0x44BBFF), 0); + lv_obj_set_flex_grow(titleLbl, 1); + + // PLAY/STOP + lv_obj_t* playBtn = mcButton(transport); + lv_obj_set_size(playBtn, 176, btnH); + lv_obj_set_style_bg_color(playBtn, lv_color_hex(0x00AA44), 0); + lv_obj_add_event_cb(playBtn, cbPlayStop, LV_EVENT_CLICKED, mc); + bs.playBtn = playBtn; + lv_obj_t* playLbl = lv_label_create(playBtn); + lv_label_set_text(playLbl, "PLAY"); + lv_obj_set_style_text_font(playLbl, fontSmall, 0); + lv_obj_center(playLbl); + bs.playLbl = playLbl; + + // CLEAR + lv_obj_t* clearBtn = mcButton(transport); + lv_obj_set_size(clearBtn, 144, btnH); + lv_obj_set_style_bg_color(clearBtn, lv_color_hex(0x331A00), 0); + lv_obj_add_event_cb(clearBtn, cbClear, LV_EVENT_CLICKED, mc); + lv_obj_t* clearLbl = lv_label_create(clearBtn); + lv_label_set_text(clearLbl, "CLEAR"); + lv_obj_set_style_text_font(clearLbl, fontSmall, 0); + lv_obj_center(clearLbl); + + // BPM - + lv_obj_t* bpmDown = mcButton(transport); + lv_obj_set_size(bpmDown, 80, btnH); + lv_obj_add_event_cb(bpmDown, cbBpmDown, LV_EVENT_CLICKED, mc); + lv_obj_t* bdLbl = lv_label_create(bpmDown); + lv_label_set_text(bdLbl, "-"); + lv_obj_set_style_text_font(bdLbl, fontSmall, 0); + lv_obj_center(bdLbl); + + // BPM label + bs.bpmLbl = lv_label_create(transport); + char bpmBuf[8]; snprintf(bpmBuf, sizeof(bpmBuf), "%d", bs.bpm); + lv_label_set_text(bs.bpmLbl, bpmBuf); + lv_obj_set_style_text_font(bs.bpmLbl, fontSmall, 0); + lv_obj_set_style_text_color(bs.bpmLbl, lv_color_hex(0xFFE000), 0); + lv_obj_set_width(bs.bpmLbl, 128); + lv_obj_set_style_text_align(bs.bpmLbl, LV_TEXT_ALIGN_CENTER, 0); + + // BPM + + lv_obj_t* bpmUp = mcButton(transport); + lv_obj_set_size(bpmUp, 80, btnH); + lv_obj_add_event_cb(bpmUp, cbBpmUp, LV_EVENT_CLICKED, mc); + lv_obj_t* buLbl = lv_label_create(bpmUp); + lv_label_set_text(buLbl, "+"); + lv_obj_set_style_text_font(buLbl, fontSmall, 0); + lv_obj_center(buLbl); + + // BPM unit label + lv_obj_t* bpmUnit = lv_label_create(transport); + lv_label_set_text(bpmUnit, "BPM"); + lv_obj_set_style_text_font(bpmUnit, fontSmall, 0); + lv_obj_set_style_text_color(bpmUnit, lv_color_hex(0x666688), 0); + + // -- Track header row (step markers only) -------------------------------- + lv_obj_t* hdrRow = mcContainer(root); + lv_obj_set_size(hdrRow, LV_PCT(100), 64); + lv_obj_set_flex_flow(hdrRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(hdrRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(hdrRow, lv_color_hex(0x0A0C14), 0); + lv_obj_set_style_bg_opa(hdrRow, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(hdrRow, 0, 0); + lv_obj_set_style_pad_all(hdrRow, 4, 0); + lv_obj_set_style_pad_gap(hdrRow, 0, 0); + lv_obj_set_scrollable(hdrRow, false); + + // Spacer to align step markers with step buttons below + lv_obj_t* hdrSpacer = mcContainer(hdrRow); + lv_obj_set_size(hdrSpacer, labelW + 4, 1); + lv_obj_set_style_bg_opa(hdrSpacer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(hdrSpacer, 0, 0); + + // Step index labels (beat markers: 1, 5, 9, 13) + for (int s = 0; s < Sequencer::NUM_STEPS; s++) { + if (s % 4 == 0) { + lv_obj_t* marker = lv_label_create(hdrRow); + char buf[4]; snprintf(buf, sizeof(buf), "%d", s + 1); + lv_label_set_text(marker, buf); + lv_obj_set_width(marker, stepW * 4 + 4 * 3); + lv_obj_set_style_text_font(marker, fontSmall, 0); + lv_obj_set_style_text_color(marker, lv_color_hex(0x666688), 0); + lv_obj_set_style_text_align(marker, LV_TEXT_ALIGN_LEFT, 0); + } + } + + // -- Step grid (4 tracks × 16 steps) -------------------------------- + lv_obj_t* gridArea = mcContainer(root); + lv_obj_set_width(gridArea, LV_PCT(100)); + lv_obj_set_flex_grow(gridArea, 1); + lv_obj_set_style_bg_color(gridArea, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(gridArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(gridArea, 0, 0); + lv_obj_set_style_pad_all(gridArea, 0, 0); + lv_obj_set_style_pad_gap(gridArea, 0, 0); + lv_obj_set_scrollable(gridArea, false); + lv_obj_set_flex_flow(gridArea, LV_FLEX_FLOW_COLUMN); + + for (int t = 0; t < Sequencer::NUM_TRACKS; t++) { + // Track row + lv_obj_t* trackRow = mcContainer(gridArea); + lv_obj_set_width(trackRow, LV_PCT(100)); + lv_obj_set_flex_grow(trackRow, 1); + lv_obj_set_flex_flow(trackRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(trackRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(trackRow, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(trackRow, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(trackRow, 0, 0); + lv_obj_set_style_pad_all(trackRow, 4, 0); + lv_obj_set_style_pad_gap(trackRow, 4, 0); + lv_obj_set_scrollable(trackRow, false); + + // Track label + lv_obj_t* trackLbl = mcContainer(trackRow); + lv_obj_set_size(trackLbl, labelW, LV_PCT(100)); + lv_obj_set_style_bg_color(trackLbl, lv_color_hex(0x0A0C14), 0); + lv_obj_set_style_bg_opa(trackLbl, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(trackLbl, 2, 0); + lv_obj_set_style_border_side(trackLbl, LV_BORDER_SIDE_RIGHT, 0); + lv_obj_set_style_border_color(trackLbl, trackColor(t), 0); + lv_obj_set_style_pad_all(trackLbl, 8, 0); + lv_obj_set_scrollable(trackLbl, false); + + lv_obj_t* tNameLbl = lv_label_create(trackLbl); + lv_label_set_text(tNameLbl, TRACK_NAMES[t]); + lv_obj_set_style_text_font(tNameLbl, fontSmall, 0); + lv_obj_set_style_text_color(tNameLbl, trackColor(t), 0); + lv_obj_center(tNameLbl); + + // 16 step buttons + for (int s = 0; s < Sequencer::NUM_STEPS; s++) { + lv_obj_t* btn = mcButton(trackRow); + lv_obj_set_size(btn, stepW, LV_PCT(90)); + lv_obj_set_style_bg_color(btn, lv_color_hex(0x1A2A40), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(btn, 2, 0); + lv_obj_set_style_pad_all(btn, 0, 0); + lv_obj_set_style_border_width(btn, 0, 0); + + // Beat boundary: slight highlight on every 4th step column + if (isBeatBoundary(s)) { + lv_obj_set_style_border_width(btn, 1, LV_PART_MAIN); + lv_obj_set_style_border_side(btn, LV_BORDER_SIDE_LEFT, LV_PART_MAIN); + lv_obj_set_style_border_color(btn, lv_color_hex(0x334455), LV_PART_MAIN); + } + + // Pack track+step into user_data + lv_obj_set_user_data(btn, (void*)(intptr_t)((t << 8) | s)); + lv_obj_add_event_cb(btn, cbStepBtn, LV_EVENT_CLICKED, mc); + + bs.stepBtn[t][s] = btn; + } + } + + // Sync UI state with current playback status + refreshPlayBtn(mc); + refreshAllStepBtns(mc); +} diff --git a/Apps/MidiController/main/Source/ViewChord.cpp b/Apps/MidiController/main/Source/ViewChord.cpp new file mode 100644 index 0000000..a925a27 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewChord.cpp @@ -0,0 +1,298 @@ +#include "MidiController.h" + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// CHORD view - Diatonic chord player +// +// 8 chord buttons (I-VII + I octave up) filling the main area. +// Control row at bottom: OCT- | OCT+ | SCALE | CLEAR +// --------------------------------------------------------------------------- + +// Diatonic chord Roman numeral labels +static const char* CHORD_LABELS[] = { "I", "ii", "iii", "IV", "V", "vi", "vii\xc2\xb0", "I+" }; +// Chord type per diatonic degree: 0=MAJ 1=MIN 2=DIM +static const int CHORD_TYPE[8] = { 0, 1, 1, 0, 0, 1, 2, 0 }; +// Diatonic scale degree index for getNoteInScale (degree 0-6, degree 7 = degree 0 +1 octave) +static const int DEGREE_IDX[8] = { 0, 1, 2, 3, 4, 5, 6, 0 }; +// Octave offset per degree (0=same octave, 1=octave up for I+) +static const int DEGREE_OCT[8] = { 0, 0, 0, 0, 0, 0, 0, 1 }; +// Per-degree color palette +static const uint32_t DEGREE_COLORS[8] = { + 0xFF3333, 0xFF8800, 0xFFFF00, 0x33CC33, + 0x3399FF, 0xAA33FF, 0xFF33CC, 0xFF3333 +}; + +// Chord voicing intervals (up to 4 notes, -1 = unused) +static const int CHORD_VOICING[3][4] = { + { 0, 4, 7, -1 }, // MAJ + { 0, 3, 7, -1 }, // MIN + { 0, 3, 6, -1 }, // DIM +}; + + +static void playChord(MidiController* mc, int degree) { + auto& ch = mc->chord_; + const int* voicing = CHORD_VOICING[CHORD_TYPE[degree]]; + int rootNote = getNoteInScale(ch.scale, DEGREE_IDX[degree], ch.octave + DEGREE_OCT[degree], ch.key); + rootNote = std::max(0, std::min(127, rootNote)); + + for (int n = 0; n < 4; n++) { + if (voicing[n] < 0) { + ch.chordNotes[degree][n] = -1; + continue; + } + int note = std::max(0, std::min(127, rootNote + voicing[n])); + ch.chordNotes[degree][n] = note; + mc->sendMidi(0x90, (uint8_t)note, 100); + } +} + +static void refreshChordBtn(MidiController* mc, int degree) { + lv_obj_t* btn = mc->chord_.chordBtn[degree]; + if (!btn) return; + bool pressed = mc->chord_.chordPlaying[degree]; + lv_color_t col = lv_color_hex(DEGREE_COLORS[degree]); + if (pressed) { + lv_obj_set_style_bg_color(btn, col, LV_PART_MAIN); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, LV_PART_MAIN); + // Dark text on bright bg + lv_obj_t* lbl = lv_obj_get_child(btn, 0); + if (lbl) lv_obj_set_style_text_color(lbl, lv_color_hex(0x080C12), LV_PART_MAIN); + } else { + lv_obj_set_style_bg_color(btn, lv_color_hex(0x1A2A40), LV_PART_MAIN); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_t* lbl = lv_obj_get_child(btn, 0); + if (lbl) lv_obj_set_style_text_color(lbl, col, LV_PART_MAIN); + } +} + +static void clearAllChordState(MidiController* mc) { + for (int i = 0; i < 8; i++) { + mc->chord_.chordPlaying[i] = false; + refreshChordBtn(mc, i); + } +} + +static void rebuildChordBtnLabels(MidiController* mc) { + auto& ch = mc->chord_; + for (int i = 0; i < 8; i++) { + if (!ch.chordBtn[i]) continue; + lv_obj_t* lblObj = lv_obj_get_child(ch.chordBtn[i], 0); + if (lblObj) { + int rootNote = std::max(0, std::min(127, getNoteInScale(ch.scale, DEGREE_IDX[i], ch.octave + DEGREE_OCT[i], ch.key))); + char noteName[8]; midiNoteName(rootNote, noteName, sizeof(noteName)); + char buf[16]; snprintf(buf, sizeof(buf), "%s\n%s", CHORD_LABELS[i], noteName); + lv_label_set_text(lblObj, buf); + } + } +} + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbChordPressed(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + lv_obj_t* btn = lv_event_get_target_obj(e); + int degree = (int)(intptr_t)lv_obj_get_user_data(btn); + mc->chord_.chordPlaying[degree] = true; + playChord(mc, degree); + refreshChordBtn(mc, degree); +} + +static void cbChordReleased(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + lv_obj_t* btn = lv_event_get_target_obj(e); + int degree = (int)(intptr_t)lv_obj_get_user_data(btn); + mc->chord_.chordPlaying[degree] = false; + // Use stored notes from press to avoid note-off mismatch if octave/scale changed + for (int n = 0; n < 4; n++) { + int note = mc->chord_.chordNotes[degree][n]; + if (note >= 0) mc->sendMidi(0x80, (uint8_t)note, 0); + } + refreshChordBtn(mc, degree); +} + +static void cbOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->stopAllNotes(); + clearAllChordState(mc); + if (mc->chord_.octave > 1) mc->chord_.octave--; + rebuildChordBtnLabels(mc); + if (mc->chord_.octLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "Oct %d", mc->chord_.octave); + lv_label_set_text(mc->chord_.octLbl, buf); + } +} +static void cbOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->stopAllNotes(); + clearAllChordState(mc); + if (mc->chord_.octave < 7) mc->chord_.octave++; + rebuildChordBtnLabels(mc); + if (mc->chord_.octLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "Oct %d", mc->chord_.octave); + lv_label_set_text(mc->chord_.octLbl, buf); + } +} + +static void cbScaleCycle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->stopAllNotes(); + clearAllChordState(mc); + mc->chord_.scale = (mc->chord_.scale + 1) % NUM_MIDI_SCALES; + rebuildChordBtnLabels(mc); + if (mc->chord_.scaleLbl) { + lv_label_set_text(mc->chord_.scaleLbl, MIDI_SCALES[mc->chord_.scale].name); + } +} + +static void cbClear(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->stopAllNotes(); + for (int i = 0; i < 8; i++) { + mc->chord_.chordPlaying[i] = false; + refreshChordBtn(mc, i); + } +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildChordView(MidiController* mc, lv_obj_t* parent) { + auto& ch = mc->chord_; + // Mode switching sends All Notes Off, so any held-chord flags from the + // previous visit are stale; rebuild the buttons in the released state. + for (auto& playing : ch.chordPlaying) playing = false; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + int ctrlH = 88; + int btnH = MC_BTN_H; + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // -- Control row (TOP) ---------------------------------------------------- + lv_obj_t* ctrl = mcRow(root, ctrlH); + + // Back + mcBackBtn(ctrl, cbBack, mc, 96); + + lv_obj_t* titleLbl = lv_label_create(ctrl); + lv_label_set_text(titleLbl, "CHORD"); + lv_obj_set_style_text_font(titleLbl, fontNormal, 0); + lv_obj_set_style_text_color(titleLbl, lv_color_hex(0x44BBFF), 0); + lv_obj_set_flex_grow(titleLbl, 1); + + // OCT - + lv_obj_t* octDown = mcButton(ctrl); + lv_obj_set_size(octDown, 128, btnH); + lv_obj_add_event_cb(octDown, cbOctDown, LV_EVENT_CLICKED, mc); + lv_obj_t* odl = lv_label_create(octDown); + lv_label_set_text(odl, "OCT-"); + lv_obj_set_style_text_font(odl, fontSmall, 0); + lv_obj_center(odl); + + // OCT label + ch.octLbl = lv_label_create(ctrl); + char obuf[8]; snprintf(obuf, sizeof(obuf), "Oct %d", ch.octave); + lv_label_set_text(ch.octLbl, obuf); + lv_obj_set_style_text_font(ch.octLbl, fontSmall, 0); + lv_obj_set_style_text_color(ch.octLbl, lv_color_hex(0x666688), 0); + lv_obj_set_width(ch.octLbl, 144); + lv_obj_set_style_text_align(ch.octLbl, LV_TEXT_ALIGN_CENTER, 0); + + // OCT + + lv_obj_t* octUp = mcButton(ctrl); + lv_obj_set_size(octUp, 128, btnH); + lv_obj_add_event_cb(octUp, cbOctUp, LV_EVENT_CLICKED, mc); + lv_obj_t* oul = lv_label_create(octUp); lv_label_set_text(oul, "OCT+"); + lv_obj_set_style_text_font(oul, fontSmall, 0); lv_obj_center(oul); + + // SCALE + lv_obj_t* scaleBtn = mcButton(ctrl); + lv_obj_set_size(scaleBtn, 224, btnH); + lv_obj_add_event_cb(scaleBtn, cbScaleCycle, LV_EVENT_CLICKED, mc); + ch.scaleLbl = lv_label_create(scaleBtn); + lv_label_set_text(ch.scaleLbl, MIDI_SCALES[ch.scale].name); + lv_obj_set_style_text_font(ch.scaleLbl, fontSmall, 0); + lv_obj_center(ch.scaleLbl); + + // CLEAR + lv_obj_t* clearBtn = mcButton(ctrl); + lv_obj_set_size(clearBtn, 176, btnH); + lv_obj_set_style_bg_color(clearBtn, lv_color_hex(0x331A00), 0); + lv_obj_add_event_cb(clearBtn, cbClear, LV_EVENT_CLICKED, mc); + lv_obj_t* clearLbl = lv_label_create(clearBtn); + lv_label_set_text(clearLbl, "CLEAR"); + lv_obj_set_style_text_font(clearLbl, fontSmall, 0); + lv_obj_center(clearLbl); + + // -- Chord button row (fills remaining space) ----------------------------- + lv_obj_t* chordRow = mcContainer(root); + lv_obj_set_width(chordRow, LV_PCT(100)); + lv_obj_set_flex_grow(chordRow, 1); + lv_obj_set_flex_flow(chordRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(chordRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(chordRow, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(chordRow, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(chordRow, 0, 0); + lv_obj_set_style_pad_all(chordRow, 8, 0); + lv_obj_set_style_pad_gap(chordRow, 8, 0); + lv_obj_set_scrollable(chordRow, false); + + for (int i = 0; i < 8; i++) { + lv_obj_t* btn = mcButton(chordRow); + lv_obj_set_flex_grow(btn, 1); + lv_obj_set_height(btn, LV_PCT(100)); + lv_obj_set_style_bg_color(btn, lv_color_hex(0x1A2A40), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(btn, lv_color_hex(DEGREE_COLORS[i]), 0); + lv_obj_set_style_border_width(btn, 8, 0); + lv_obj_set_style_radius(btn, 16, 0); + lv_obj_set_style_pad_all(btn, 8, 0); + lv_obj_set_user_data(btn, (void*)(intptr_t)i); + lv_obj_add_event_cb(btn, cbChordPressed, LV_EVENT_PRESSED, mc); + lv_obj_add_event_cb(btn, cbChordReleased, LV_EVENT_RELEASED, mc); + + // Label: Roman numeral + root note name with octave (two lines) + int rootNote = std::max(0, std::min(127, getNoteInScale(ch.scale, DEGREE_IDX[i], ch.octave + DEGREE_OCT[i], ch.key))); + char rootName[8]; midiNoteName(rootNote, rootName, sizeof(rootName)); + char lbuf[16]; snprintf(lbuf, sizeof(lbuf), "%s\n%s", CHORD_LABELS[i], rootName); + lv_obj_t* lbl = lv_label_create(btn); + lv_label_set_text(lbl, lbuf); + lv_obj_set_style_text_font(lbl, fontSmall, 0); + lv_obj_set_style_text_color(lbl, lv_color_hex(DEGREE_COLORS[i]), 0); + lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(lbl); + lv_obj_set_clickable(lbl, false); + + ch.chordBtn[i] = btn; + } + +} diff --git a/Apps/MidiController/main/Source/ViewDimensions.cpp b/Apps/MidiController/main/Source/ViewDimensions.cpp new file mode 100644 index 0000000..36676d5 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewDimensions.cpp @@ -0,0 +1,575 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// DIMENSIONS view - parametric equation sequencer (20 equations) +// +// Layout (flex column): +// [row1: < | EQ- | Eq:N | EQ+ | PLAY | RESET] +// [row2: A- | A:N | A+ B- | B:N | B+] +// [row3: C- | C:N | C+ D- | D:N | D+] +// [outArea flex_grow: px:N py:N pz:N t:N] +// [row5: OCT- | Oct N | OCT+ RND- | Rnd:N | RND+ BPM- | N | BPM+] +// --------------------------------------------------------------------------- + + +// --------------------------------------------------------------------------- +// Parametric equations +// --------------------------------------------------------------------------- + +static inline float sq(float x) { return x * x; } + +// Arduino random() compat - uniform int in [lo, hi) +static inline int rng_range(int lo, int hi) { + if (hi <= lo) return lo; + return lo + (int)((uint32_t)(esp_random()) % (uint32_t)(hi - lo)); +} + +static void dimsEval(int eq, float t, float a, float b, float c, float d, + float rnd, float x, float y, + float& px, float& py, float& pz) { + if (a == 0) a = 0.001f; + if (b == 0) b = 0.001f; + if (c == 0) c = 0.001f; + if (d == 0) d = 0.001f; + + constexpr float PI = 3.14159265f; + float px_f, py_f, pz_f; + + switch (eq) { + case 1: + px_f = x + t * sinf(a * t + PI/2.0f); + py_f = y + t * sinf(b * t); + pz_f = t * cosf(a * t / PI * c); + break; + case 2: { + float denom2 = (a - b); + if (fabsf(denom2) < 0.01f) denom2 = (denom2 >= 0.0f) ? 0.01f : -0.01f; + px_f = x + t * sinf(a * t + PI/2.0f) + cosf(sq(a+b)/PI) * tanf(a); + py_f = y + t * sinf(b * t) * (tanf((t * a)/denom2) / (b * 4.0f)); + pz_f = t * cosf(a * t / PI); + } + break; + case 3: + px_f = x + t * sinf(a * t + PI/(c + 1.0f)) + cosf(sq(a+b)/(c + 0.5f)); + py_f = y + t * sinf(c * t) * (tanf((t * a)/(c-b + 0.12f)) / ((b * 4.0f) + 0.5f)); + pz_f = t * sinf(a * t / PI); + break; + case 4: + px_f = x + t * sinf(a * t + PI/c) + cosf(sq(PI/2.0f)/c); + py_f = y + t * sinf(b * t/2.0f) * cosf(t/PI * a); + pz_f = t * sinf(c * t / PI); + break; + case 5: + px_f = x + t * sinf(a * t/2.0f) + (rnd * (PI / (float)rng_range(2,30) * 1.75f)); + py_f = y + t * sinf(b * t) + (rnd * (PI / (float)rng_range(5,25) * c)); + pz_f = t * cosf(a * t / PI * c); + break; + case 6: + px_f = x + (float)rng_range(-64,64); + py_f = y + (float)rng_range(-64,64); + pz_f = t * (float)rng_range(0,127); + break; + case 7: + px_f = x + t * sinf(a * t + PI/3.0f) * (PI / powf(b, 2.0f)); + py_f = y + t * sinf(b * t); + pz_f = t * cosf(a * t / PI * c * t); + break; + case 8: + px_f = x + t * sinf(a * t + PI/2.0f) - (t * cosf(a * t + PI/4.0f)); + py_f = y + t * sinf(b * t) + (y - t * cosf(0.25f * b * t)); + pz_f = t * cosf(a * t / PI * c); + break; + case 9: + px_f = x + t * sinf(a * t + PI/25.0f) * (d * 0.5f); + py_f = y + t * cosf(-b * t + PI/130.0f) * (d * 0.5f) + t * sinf(a * t + PI/25.0f); + pz_f = t * sinf(t / PI * c); + break; + case 10: + px_f = x + t * sinf(a * t + PI/2.0f) * logf(16.0f)/b * 2.0f; + py_f = y + t * sinf(b * t) + sinf(d * t * 100.0f) + tanf(PI * t); + pz_f = t * 0.5f * sinf(t / PI * c + 0.25f); + break; + case 11: + px_f = x + t * sinf(a * t + PI/3.0f * 0.025f); + py_f = y + t * sinf(b * 0.05f * t * cosf(b * t * 0.05f)) + rnd; + pz_f = t * tanf(a * t / PI * c * 0.025f) + rnd * 1.0f + d; + break; + case 12: + px_f = x + t * sinf(a * t + PI/2.0f); + py_f = y + (float)rng_range(-64,64) * rnd * 1.0f/64.0f; + pz_f = t * cosf(d * t / PI * c) * 1.0f + b * sinf(t * 2.25f); + break; + case 13: { + float bmod = (b == 0) ? 1.0f : b; + px_f = x + t * sinf(fmodf(a, bmod) * t) + sinf(PI/3.0f * c); + py_f = y + t * sinf(t * c) + cosf(fmodf(a, bmod)) * 20.25f; + pz_f = t * cosf(a * t / PI); + break; + } + case 14: + px_f = x + (float)rng_range(-64,64) * rnd * 1.0f/64.0f; + py_f = y + t * sinf(a * t + PI/2.0f); + pz_f = t * sinf(b * t / PI) * 2.0f + b * cosf(t * 20.0f); + break; + case 15: + px_f = x + t * tanf(a * t + PI * sinf(b * t)) * 0.75f; + py_f = y + t * tanf(b * t) - cosf(a * t * 0.5f) * 0.75f; + pz_f = t * cosf(2.0f * a * t / PI * c); + break; + case 16: + px_f = x + t * sinf(a * t + PI/3.0f) - (1.75f * t) * sinf(1.5f * a * t + PI/3.0f) * 1.0f/c * 0.5f; + py_f = y + t * sinf(b * t + PI/3.0f) - (1.75f * t) * sinf(1.5f * b * t + PI/3.0f) * 1.0f/c * 0.5f; + pz_f = y + t * sinf(d * t + PI/3.0f) - (1.75f * t) * cosf(1.5f * d * t + PI/2.0f) * 1.0f/c * 0.5f; + break; + case 17: + px_f = x + t * sinf(a * t + PI); + py_f = y + t * tanf(b * t - PI); + pz_f = t * cosf(a * t / PI * c); + break; + case 18: { + int r = (int)fabsf(rnd); + if (r == 0) r = 1; + px_f = x + (float)rng_range(-r, r); + py_f = y + (float)rng_range(-r, r); + pz_f = t * (float)rng_range(0, r); + break; + } + case 19: + px_f = x + t * sinf(a * t + PI/2.0f) + cosf(sq(a+b)/PI) * tanf(a) + rnd; + py_f = y + t * sinf(b/c * t) * (tanf((t * a)/(a+b)) / (b * 4.0f)) + d/2.25f; + pz_f = t * tanf(c * t / PI); + break; + case 20: + px_f = x + t * sinf(a * t + (PI / powf(c, 3.0f))); + py_f = y + (t * sinf(b * t/2.0f) * (tanf(t/PI * a) * 0.25f)) - d; + pz_f = t/2.25f * tanf(c * t/4.0f) * rnd; + break; + default: + px_f = x; py_f = y; pz_f = 64.0f; + break; + } + + // Clamp 0-127; guard NaN/Inf + auto guard = [](float v) -> float { + if (!std::isfinite(v)) return 64.0f; + return std::max(0.0f, std::min(127.0f, v)); + }; + px = guard(px_f); + py = guard(py_f); + pz = guard(pz_f); +} + +// px (0-127) → BPM-derived ms per step (10 clock-division tiers) +static uint32_t pxToMs(float px_val, int bpm) { + // Tiers range from a whole note (16 sixteenths) to a 32nd note (0.5). + static const float DIVIDERS[10] = { + 16.0f, 12.0f, 8.0f, 6.0f, 4.0f, 3.0f, 2.0f, 4.0f/3.0f, 1.0f, 0.5f + }; + int idx = (int)(px_val / 127.0f * 9.0f + 0.5f); + if (idx < 0) idx = 0; + if (idx > 9) idx = 9; + float beatMs = 60000.0f / (float)bpm; + uint32_t ms = (uint32_t)(beatMs * DIVIDERS[idx] / 4.0f); + if (ms < 20) ms = 20; + return ms; +} + +// --------------------------------------------------------------------------- +// Timer callback +// --------------------------------------------------------------------------- + +static void cbDimTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + auto& ds = mc->dim_; + if (!ds.playing) return; + + uint32_t now = lv_tick_get(); + + // Note-off + if (ds.lastNote >= 0 && now >= ds.noteOffMs) { + mc->sendMidi(0x80, (uint8_t)ds.lastNote, 0); + ds.lastNote = -1; + } + + if (now < ds.tickMs) return; + + // Evaluate equation + dimsEval(ds.equation, ds.t, ds.a, ds.b, ds.c, ds.d, ds.rnd, + ds.xOff, ds.yOff, ds.px, ds.py, ds.pz); + + // Advance t (wraps at t2) + ds.t += ds.gran; + if (ds.t > ds.t2) ds.t = 0.0f; + + // py → note (0-127 mapped to C2-C7, chromatic, before the octave offset) + int note = (int)(ds.py / 127.0f * 60.0f) + 36; // MIDI 36-96 + note += ds.octave * 12; + if (note < 0) note = 0; + if (note > 127) note = 127; + + // pz → velocity + int vel = std::max(1, std::min(127, (int)ds.pz)); + + // Trigger if py above rest threshold (low gate: ~4%) + if (ds.py >= 5.0f) { + if (ds.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)ds.lastNote, 0); + } + mc->sendMidi(0x90, (uint8_t)note, (uint8_t)vel); + ds.lastNote = note; + ds.noteOffMs = now + 80; // 80ms gate + } + + // Schedule next tick based on px + uint32_t stepMs = pxToMs(ds.px, ds.bpm); + ds.tickMs = now + stepMs; + + // Update output label + if (ds.outLbl) { + char buf[48]; + snprintf(buf, sizeof(buf), "px:%-3d py:%-3d pz:%-3d t:%-5.1f", + (int)ds.px, (int)ds.py, (int)ds.pz, ds.t); + lv_label_set_text(ds.outLbl, buf); + } +} + +// Forward declarations for public encoder entry points (defined at file end) +void dimTogglePlay(MidiController* mc); +void dimReset (MidiController* mc); + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbDimBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbDimPlay(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) dimTogglePlay(mc); +} + +static void cbDimReset(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + dimReset(mc); +} + +static void cbDimEqDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ds = mc->dim_; + ds.equation = (ds.equation > 1) ? ds.equation - 1 : 20; + if (ds.eqLbl) { + char buf[16]; snprintf(buf, sizeof(buf), "Eq:%d", ds.equation); + lv_label_set_text(ds.eqLbl, buf); + } +} +static void cbDimEqUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ds = mc->dim_; + ds.equation = (ds.equation < 20) ? ds.equation + 1 : 1; + if (ds.eqLbl) { + char buf[16]; snprintf(buf, sizeof(buf), "Eq:%d", ds.equation); + lv_label_set_text(ds.eqLbl, buf); + } +} + +// Generic param adjust helpers - user_data encodes which param + direction +// We use a small enum packing: low bit = direction (0=down 1=up), rest = param idx (0=a,1=b,2=c,3=d) +struct DimParamEvt { MidiController* mc; int param; int dir; }; +static DimParamEvt s_paramEvt[8]; // 4 params × 2 directions + +static void cbDimParam(lv_event_t* e) { + auto* pe = static_cast(lv_event_get_user_data(e)); + if (!pe || !pe->mc) return; + auto& ds = pe->mc->dim_; + float* p[4] = {&ds.a, &ds.b, &ds.c, &ds.d}; + lv_obj_t** lbls[4] = {&ds.aLbl, &ds.bLbl, &ds.cLbl, &ds.dLbl}; + static const char* names[4] = {"A", "B", "C", "D"}; + float& v = *p[pe->param]; + v += (pe->dir > 0) ? 1.0f : -1.0f; + if (v < 0) v = 0; + if (v > 100) v = 100; + if (*lbls[pe->param]) { + char buf[12]; snprintf(buf, sizeof(buf), "%s:%.0f", names[pe->param], v); + lv_label_set_text(*lbls[pe->param], buf); + } +} + +static void cbDimOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ds = mc->dim_; + if (ds.octave > -4) ds.octave--; + if (ds.octLbl) { + char buf[10]; snprintf(buf, sizeof(buf), "Oct%+d", ds.octave); + lv_label_set_text(ds.octLbl, buf); + } +} +static void cbDimOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ds = mc->dim_; + if (ds.octave < 4) ds.octave++; + if (ds.octLbl) { + char buf[10]; snprintf(buf, sizeof(buf), "Oct%+d", ds.octave); + lv_label_set_text(ds.octLbl, buf); + } +} + +static void cbDimRndDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ds = mc->dim_; + if (ds.rnd > 0) ds.rnd -= 5.0f; + if (ds.rnd < 0) ds.rnd = 0; + if (ds.rndLbl) { + char buf[10]; snprintf(buf, sizeof(buf), "R:%.0f", ds.rnd); + lv_label_set_text(ds.rndLbl, buf); + } +} +static void cbDimRndUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ds = mc->dim_; + if (ds.rnd < 64) ds.rnd += 5.0f; + if (ds.rndLbl) { + char buf[10]; snprintf(buf, sizeof(buf), "R:%.0f", ds.rnd); + lv_label_set_text(ds.rndLbl, buf); + } +} + +static void cbDimBpmDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ds = mc->dim_; + if (ds.bpm > 40) ds.bpm -= 5; + if (ds.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", ds.bpm); + lv_label_set_text(ds.bpmLbl, buf); + } +} +static void cbDimBpmUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ds = mc->dim_; + if (ds.bpm < 280) ds.bpm += 5; + if (ds.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", ds.bpm); + lv_label_set_text(ds.bpmLbl, buf); + } +} + +// --------------------------------------------------------------------------- +// Build +// --------------------------------------------------------------------------- + +void buildDimensionsView(MidiController* mc, lv_obj_t* parent) { + auto& ds = mc->dim_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + + int smallBtnW = 88; + int valW = 152; + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C14), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 8, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + auto makeRow = [&]() -> lv_obj_t* { return mcRow(root); }; + + auto makeBtn = [&](lv_obj_t* par, const char* txt, int w, lv_event_cb_t cb, void* ud) -> lv_obj_t* { + return mcSmallBtn(par, txt, w, cb, ud); + }; + + auto makeLbl = [&](lv_obj_t* par, const char* txt, int w, lv_color_t col) -> lv_obj_t* { + return mcValue(par, txt, w, col); + }; + + auto makeSepLbl = [&](lv_obj_t* par) { + lv_obj_t* sep = lv_label_create(par); + lv_label_set_text(sep, "|"); + lv_obj_set_style_text_font(sep, fontSmall, 0); + lv_obj_set_style_text_color(sep, lv_color_hex(0x334455), 0); + }; + + // -- Row 1: [<] [EQ-] [Eq:N] [EQ+] | [PLAY] [RESET] ----------------------- + { + lv_obj_t* row = makeRow(); + mcBackBtn(row, cbDimBack, mc, MC_W_S); + lv_obj_t* titleLbl = lv_label_create(row); + lv_label_set_text(titleLbl, "DIMENSIONS"); + lv_obj_set_style_text_font(titleLbl, fontNormal, 0); + lv_obj_set_style_text_color(titleLbl, lv_color_hex(0x44BBFF), 0); + lv_obj_set_flex_grow(titleLbl, 1); + makeBtn(row, "<", smallBtnW, cbDimEqDown, mc); + char buf[16]; snprintf(buf, sizeof(buf), "Eq:%d", ds.equation); + ds.eqLbl = makeLbl(row, buf, 144, lv_color_hex(0xFF8844)); + makeBtn(row, ">", smallBtnW, cbDimEqUp, mc); + makeSepLbl(row); + lv_obj_t* playBtn = makeBtn(row, ds.playing ? "STOP" : "PLAY", 176, cbDimPlay, mc); + ds.playLbl = lv_obj_get_child(playBtn, 0); + makeBtn(row, "RESET", 192, cbDimReset, mc); + } + + // -- Row 2: A and B params -------------------------------------------------- + { + lv_obj_t* row = makeRow(); + // A + s_paramEvt[0] = {mc, 0, -1}; + s_paramEvt[1] = {mc, 0, +1}; + makeBtn(row, "A-", smallBtnW, cbDimParam, &s_paramEvt[0]); + char bufA[12]; snprintf(bufA, sizeof(bufA), "A:%.0f", ds.a); + ds.aLbl = makeLbl(row, bufA, valW, lv_color_hex(0x88DDFF)); + makeBtn(row, "A+", smallBtnW, cbDimParam, &s_paramEvt[1]); + makeSepLbl(row); + // B + s_paramEvt[2] = {mc, 1, -1}; + s_paramEvt[3] = {mc, 1, +1}; + makeBtn(row, "B-", smallBtnW, cbDimParam, &s_paramEvt[2]); + char bufB[12]; snprintf(bufB, sizeof(bufB), "B:%.0f", ds.b); + ds.bLbl = makeLbl(row, bufB, valW, lv_color_hex(0x88FFCC)); + makeBtn(row, "B+", smallBtnW, cbDimParam, &s_paramEvt[3]); + } + + // -- Row 3: C and D params -------------------------------------------------- + { + lv_obj_t* row = makeRow(); + // C + s_paramEvt[4] = {mc, 2, -1}; + s_paramEvt[5] = {mc, 2, +1}; + makeBtn(row, "C-", smallBtnW, cbDimParam, &s_paramEvt[4]); + char bufC[12]; snprintf(bufC, sizeof(bufC), "C:%.0f", ds.c); + ds.cLbl = makeLbl(row, bufC, valW, lv_color_hex(0xFFCC88)); + makeBtn(row, "C+", smallBtnW, cbDimParam, &s_paramEvt[5]); + makeSepLbl(row); + // D + s_paramEvt[6] = {mc, 3, -1}; + s_paramEvt[7] = {mc, 3, +1}; + makeBtn(row, "D-", smallBtnW, cbDimParam, &s_paramEvt[6]); + char bufD[12]; snprintf(bufD, sizeof(bufD), "D:%.0f", ds.d); + ds.dLbl = makeLbl(row, bufD, valW, lv_color_hex(0xFF88CC)); + makeBtn(row, "D+", smallBtnW, cbDimParam, &s_paramEvt[7]); + } + + // -- Output area (flex_grow) ------------------------------------------------ + { + lv_obj_t* outArea = mcContainer(root); + lv_obj_set_width(outArea, LV_PCT(100)); + lv_obj_set_flex_grow(outArea, 1); + lv_obj_set_style_bg_color(outArea, lv_color_hex(0x05080F), 0); + lv_obj_set_style_bg_opa(outArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(outArea, 0, 0); + lv_obj_set_style_pad_all(outArea, 24, 0); + lv_obj_set_scrollable(outArea, false); + lv_obj_set_flex_flow(outArea, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(outArea, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + ds.outLbl = lv_label_create(outArea); + lv_label_set_text(ds.outLbl, "px: -- py: -- pz: -- t: --"); + lv_obj_set_style_text_font(ds.outLbl, fontNormal, 0); + lv_obj_set_style_text_color(ds.outLbl, lv_color_hex(0x44BBEE), 0); + lv_obj_set_width(ds.outLbl, LV_PCT(100)); + lv_obj_set_style_text_align(ds.outLbl, LV_TEXT_ALIGN_CENTER, 0); + } + + // -- Row 5: OCT RND BPM -------------------------------------------------- + { + lv_obj_t* row = makeRow(); + // Octave + makeBtn(row, "<", smallBtnW, cbDimOctDown, mc); + char bufOct[10]; snprintf(bufOct, sizeof(bufOct), "Oct%+d", ds.octave); + ds.octLbl = makeLbl(row, bufOct, 168, lv_color_hex(0xAAFF88)); + makeBtn(row, ">", smallBtnW, cbDimOctUp, mc); + makeSepLbl(row); + // Rnd + makeBtn(row, "<", smallBtnW, cbDimRndDown, mc); + char bufRnd[10]; snprintf(bufRnd, sizeof(bufRnd), "R:%.0f", ds.rnd); + ds.rndLbl = makeLbl(row, bufRnd, 144, lv_color_hex(0xFFAA44)); + makeBtn(row, ">", smallBtnW, cbDimRndUp, mc); + makeSepLbl(row); + // BPM + makeBtn(row, "<", smallBtnW, cbDimBpmDown, mc); + char bufBpm[8]; snprintf(bufBpm, sizeof(bufBpm), "%d", ds.bpm); + ds.bpmLbl = makeLbl(row, bufBpm, 144, lv_color_hex(0xFFE000)); + makeBtn(row, ">", smallBtnW, cbDimBpmUp, mc); + } + + // -- Start timer (20ms poll) ------------------------------------------------ + mc->dimTimer_ = lv_timer_create(cbDimTimer, 20, mc); + ds.tickMs = lv_tick_get(); +} + +// --------------------------------------------------------------------------- +// Public encoder entry points (called from EncoderInput.cpp) +// --------------------------------------------------------------------------- + +void dimTogglePlay(MidiController* mc) { + auto& ds = mc->dim_; + ds.playing = !ds.playing; + if (!ds.playing && ds.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)ds.lastNote, 0); + ds.lastNote = -1; + } + if (ds.playing) ds.tickMs = lv_tick_get(); + if (ds.playLbl) lv_label_set_text(ds.playLbl, ds.playing ? "STOP" : "PLAY"); +} + +void dimReset(MidiController* mc) { + auto& ds = mc->dim_; + ds.t = 0.0f; + ds.a = 50; ds.b = 50; ds.c = 50; ds.d = 50; + if (ds.aLbl) lv_label_set_text(ds.aLbl, "A:50"); + if (ds.bLbl) lv_label_set_text(ds.bLbl, "B:50"); + if (ds.cLbl) lv_label_set_text(ds.cLbl, "C:50"); + if (ds.dLbl) lv_label_set_text(ds.dLbl, "D:50"); + if (ds.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)ds.lastNote, 0); + ds.lastNote = -1; + } +} + +void dimChangeOctave(MidiController* mc, int delta) { + auto& ds = mc->dim_; + int oct = ds.octave + delta; + if (oct < -4) oct = -4; + if (oct > 4) oct = 4; + ds.octave = oct; + if (ds.octLbl) { + char buf[10]; snprintf(buf, sizeof(buf), "Oct%+d", oct); + lv_label_set_text(ds.octLbl, buf); + } +} + +void dimChangeRandom(MidiController* mc, int delta) { + auto& ds = mc->dim_; + float r = ds.rnd + (float)delta; + if (r < 0) r = 0; + if (r > 64) r = 64; + ds.rnd = r; + if (ds.rndLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "R:%.0f", r); + lv_label_set_text(ds.rndLbl, buf); + } +} diff --git a/Apps/MidiController/main/Source/ViewDrop.cpp b/Apps/MidiController/main/Source/ViewDrop.cpp new file mode 100644 index 0000000..99d84d5 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewDrop.cpp @@ -0,0 +1,515 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// DROP view - Gravity-drop physics + platform note triggering +// +// Layout (flex column): +// [physics area - flex_grow, touch to drop/place] +// [control row: < | MODE | OCT- | Oct N | OCT+ | CLEAR | SCALE | KEY- | KEY+ | status] +// +// Two modes: +// DROP (default): touch spawns a ball at touch point +// EDIT: touch places a horizontal platform (up to MAX_PLATFORMS); CLEAR removes all +// --------------------------------------------------------------------------- + + +static const uint32_t DROP_BALL_COLORS[] = { + 0xFF8800, 0xFF4444, 0x44FFAA, 0xAA44FF, + 0xFFFF44, 0x44AAFF, 0xFF44AA, 0x44FF44 +}; + +static constexpr float GRAVITY_BASE = 0.16f; +static constexpr float BOUNCE_COEFF = 0.58f; +static constexpr float FRICTION = 0.99f; +static constexpr float BALL_LIFE_MS = 14000.0f; + +// Platform geometry is expressed at the 220px reference width used by the +// physics scaling. The line in the draw callback and the collision half-height +// must stay in sync, or balls visibly sink into or float above a platform. +static constexpr float PLATFORM_HALF_H = 4.0f; +static constexpr float PLATFORM_W_FRACTION = 0.16f; + +// --------------------------------------------------------------------------- +// Physics helpers +// --------------------------------------------------------------------------- + +struct Vec2 { float x, y; }; +static float dot(Vec2 a, Vec2 b) { return a.x*b.x + a.y*b.y; } + +static void updatePlatformNote(MidiController* mc, int p) { + auto& dr = mc->drop_; + int degree = (p * 2) % 7; + dr.platforms[p].note = getNoteInScale(dr.scale, degree, dr.octave, dr.key); +} + +static void refreshPlatformLabel(MidiController* mc, int p) { + auto& dr = mc->drop_; + if (!dr.canvas || p < 0 || p >= dr.numPlatforms) return; + auto& plt = dr.platforms[p]; + + // Create label widget if it doesn't exist yet + if (!dr.platLbl[p]) { + lv_obj_t* lbl = lv_label_create(dr.canvas); + lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(lbl, lvgl_get_text_font(FONT_SIZE_SMALL), 0); + lv_obj_set_style_bg_color(lbl, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(lbl, LV_OPA_70, 0); + lv_obj_set_style_border_width(lbl, 0, 0); + lv_obj_set_style_pad_all(lbl, 4, 0); + lv_obj_set_style_radius(lbl, 8, 0); + lv_obj_set_scrollable(lbl, false); + lv_obj_set_clickable(lbl, false); + dr.platLbl[p] = lbl; + } + + char noteName[8]; midiNoteName(plt.note, noteName, sizeof(noteName)); + lv_label_set_text(dr.platLbl[p], noteName); + // Size to content so dark bg pill fits text exactly + lv_obj_set_size(dr.platLbl[p], LV_SIZE_CONTENT, LV_SIZE_CONTENT); + // Let LVGL compute content size first, then center on platform + lv_obj_update_layout(dr.platLbl[p]); + lv_coord_t lblW = lv_obj_get_width(dr.platLbl[p]); + lv_coord_t lblH = lv_obj_get_height(dr.platLbl[p]); + lv_obj_set_pos(dr.platLbl[p], + (lv_coord_t)(plt.x - lblW / 2), + (lv_coord_t)(plt.y - lblH / 2)); +} + +static void rebuildPlatformNotes(MidiController* mc) { + auto& dr = mc->drop_; + for (int p = 0; p < dr.numPlatforms; p++) { + updatePlatformNote(mc, p); + refreshPlatformLabel(mc, p); + } + // Platform line colours are derived from their notes, so the canvas needs + // a repaint even though only the labels changed. + if (dr.canvas) lv_obj_invalidate(dr.canvas); +} + +// --------------------------------------------------------------------------- +// Draw event +// --------------------------------------------------------------------------- + +static void cbDropDraw(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& dr = mc->drop_; + + lv_obj_t* obj = lv_event_get_target_obj(e); + lv_layer_t* layer = lv_event_get_layer(e); + + lv_area_t area; + lv_obj_get_coords(obj, &area); + int32_t ox = area.x1, oy = area.y1; + + // Background + lv_draw_fill_dsc_t bg; + lv_draw_fill_dsc_init(&bg); + bg.color = lv_color_hex(0x080C12); + bg.opa = LV_OPA_COVER; + lv_draw_fill(layer, &bg, &area); + + if (dr.numPlatforms == 0) return; + + // Note-based platform colors (hue cycle per semitone) + static const uint32_t NOTE_COLORS[12] = { + 0xFF3333, 0xFF6600, 0xFFAA00, 0xFFFF00, + 0x88FF00, 0x00FF88, 0x00FFFF, 0x0088FF, + 0x0033FF, 0x8800FF, 0xFF00FF, 0xFF0088 + }; + + // Draw platforms - line width matches physics platH (half-thickness × 2) + float speedScale = (dr.canvasW > 0) ? (float)dr.canvasW / 220.0f : 1.0f; + lv_draw_line_dsc_t line; + lv_draw_line_dsc_init(&line); + line.width = std::max(2, (int)(PLATFORM_HALF_H * 2.0f * speedScale)); + line.opa = LV_OPA_COVER; + + for (int p = 0; p < dr.numPlatforms; p++) { + auto& plt = dr.platforms[p]; + float cosA = cosf(plt.angle); + float sinA = sinf(plt.angle); + float hw = plt.w * 0.5f; + line.color = lv_color_hex(NOTE_COLORS[plt.note % 12]); + line.p1.x = (lv_value_precise_t)(ox + plt.x - hw * cosA); + line.p1.y = (lv_value_precise_t)(oy + plt.y - hw * sinA); + line.p2.x = (lv_value_precise_t)(ox + plt.x + hw * cosA); + line.p2.y = (lv_value_precise_t)(oy + plt.y + hw * sinA); + lv_draw_line(layer, &line); + } +} + +// --------------------------------------------------------------------------- +// Physics timer +// --------------------------------------------------------------------------- + +static void cbDropTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + auto& dr = mc->drop_; + if (!dr.canvas || dr.canvasW <= 0 || dr.canvasH <= 0) return; + + float cW = (float)dr.canvasW; + float cH = (float)dr.canvasH; + float speedScale = cW / 220.0f; + float gravity = GRAVITY_BASE * speedScale; + // Platform half-thickness scales with display so balls hit reliably + float platH = PLATFORM_HALF_H * speedScale; + int ballR = 16; + uint32_t nowMs = lv_tick_get(); + + for (int b = 0; b < MidiController::DropState::MAX_BALLS; b++) { + auto& ball = dr.balls[b]; + if (!ball.active) continue; + + if ((nowMs - ball.spawnedMs) > (uint32_t)BALL_LIFE_MS) { + ball.active = false; + continue; + } + + ball.vy += gravity; + ball.vx *= FRICTION; + ball.x += ball.vx; + ball.y += ball.vy; + + // Floor bounce + if (ball.y >= cH - ballR) { + ball.y = cH - ballR; + ball.vy = -ball.vy * BOUNCE_COEFF; + if (fabsf(ball.vy) < 0.5f * speedScale) ball.vy = 0; + } + // Wall clamp + if (ball.x < ballR) ball.x = ballR; + if (ball.x > cW - ballR) ball.x = cW - ballR; + + // Platform collisions + for (int p = 0; p < dr.numPlatforms; p++) { + auto& plt = dr.platforms[p]; + float cosA = cosf(plt.angle); + float sinA = sinf(plt.angle); + float dx = ball.x - plt.x; + float dy = ball.y - plt.y; + float localX = dx * cosA + dy * sinA; + float localY = -dx * sinA + dy * cosA; + float halfW = plt.w * 0.5f; + + if (fabsf(localX) < halfW && localY >= -platH && localY <= platH) { + // Normal perpendicular to platform, pointing toward the ball + Vec2 rawNormal = {-sinA, cosA}; + Vec2 vel = {ball.vx, ball.vy}; + float rawVn = dot(vel, rawNormal); + // Pick the normal direction that opposes ball's velocity + Vec2 normal = (rawVn < 0) ? rawNormal : Vec2{sinA, -cosA}; + float vn = dot(vel, normal); + if (vn < 0) { + ball.vx -= 2.0f * vn * normal.x; + ball.vy -= 2.0f * vn * normal.y; + ball.vx *= BOUNCE_COEFF; + ball.vy *= BOUNCE_COEFF; + // Push ball out along the normal so it clears the platform surface + ball.x = plt.x + (localX) * cosA + (platH + 1.0f) * normal.x; + ball.y = plt.y + (localX) * sinA + (platH + 1.0f) * normal.y; + + if (fabsf(vn) > 1.0f * speedScale) { + int vel_midi = std::min(127, 70 + (int)(fabsf(vn) / speedScale * 5)); + mc->sendMidi(0x90, (uint8_t)plt.note, vel_midi); + mc->sendMidi(0x80, (uint8_t)plt.note, 0); + } + } + } + } + } + + // Move the ball widgets. Moving/hiding a child invalidates the old and new + // areas and LVGL repaints the canvas background there, so there is no need + // to repaint the entire canvas at 60 fps. Full repaints are requested only + // when the static scene (platforms/labels) changes. + for (int b = 0; b < MidiController::DropState::MAX_BALLS; b++) { + if (dr.ballObj[b]) { + if (dr.balls[b].active) { + lv_obj_set_pos(dr.ballObj[b], + (lv_coord_t)(dr.balls[b].x - ballR), + (lv_coord_t)(dr.balls[b].y - ballR)); + lv_obj_set_hidden(dr.ballObj[b], false); + } else { + lv_obj_set_hidden(dr.ballObj[b], true); + } + } + } +} + +// --------------------------------------------------------------------------- +// Canvas size event - just update dimensions, no auto-platform creation +// --------------------------------------------------------------------------- + +static void cbCanvasSize(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& dr = mc->drop_; + lv_obj_t* obj = lv_event_get_target_obj(e); + int w = (int)lv_obj_get_content_width(obj); + int h = (int)lv_obj_get_content_height(obj); + if (w <= 0 || h <= 0) return; + + // If canvas size changed, scale existing platform positions + if (dr.canvasW > 0 && dr.canvasH > 0 && (w != dr.canvasW || h != dr.canvasH)) { + float sx = (float)w / dr.canvasW; + float sy = (float)h / dr.canvasH; + for (int p = 0; p < dr.numPlatforms; p++) { + dr.platforms[p].x *= sx; + dr.platforms[p].y *= sy; + dr.platforms[p].w *= sx; + refreshPlatformLabel(mc, p); + } + } + dr.canvasW = w; + dr.canvasH = h; +} + +// --------------------------------------------------------------------------- +// Touch callback - spawns ball (DROP mode) or places platform (EDIT mode) +// --------------------------------------------------------------------------- + +static void cbCanvasTouch(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& dr = mc->drop_; + if (dr.canvasW <= 0 || dr.canvasH <= 0) return; + + lv_indev_t* indev = lv_event_get_indev(e); + if (!indev) return; + lv_point_t pt; + lv_indev_get_point(indev, &pt); + + lv_area_t area; + lv_obj_get_coords(dr.canvas, &area); + float lx = (float)((int)pt.x - (int)area.x1); + float ly = (float)((int)pt.y - (int)area.y1); + if (lx < 0 || ly < 0 || lx > dr.canvasW || ly > dr.canvasH) return; + + if (dr.editMode) { + // Place a new horizontal platform at touch point (up to MAX_PLATFORMS) + if (dr.numPlatforms >= MidiController::DropState::MAX_PLATFORMS) return; + int p = dr.numPlatforms; + dr.platforms[p].x = lx; + dr.platforms[p].y = ly; + dr.platforms[p].w = (float)dr.canvasW * PLATFORM_W_FRACTION; + dr.platforms[p].angle = 0.0f; // always horizontal + updatePlatformNote(mc, p); + dr.numPlatforms++; + refreshPlatformLabel(mc, p); + lv_obj_invalidate(dr.canvas); + } else { + // Spawn a ball at touch point + int ballR = 16; + for (int b = 0; b < MidiController::DropState::MAX_BALLS; b++) { + if (!dr.balls[b].active) { + dr.balls[b].x = lx; + dr.balls[b].y = ly; + float vx = ((float)(esp_random() % 201) - 100.0f) * 0.012f * ((float)dr.canvasW / 220.0f); + dr.balls[b].vx = vx; + dr.balls[b].vy = 0.0f; + dr.balls[b].active = true; + dr.balls[b].spawnedMs = lv_tick_get(); + if (!dr.ballObj[b]) { + lv_obj_t* w = mcContainer(dr.canvas); + lv_obj_set_size(w, ballR * 2, ballR * 2); + lv_obj_set_style_radius(w, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(w, lv_color_hex(DROP_BALL_COLORS[b % 8]), 0); + lv_obj_set_style_bg_opa(w, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(w, 0, 0); + lv_obj_set_style_pad_all(w, 0, 0); + lv_obj_set_scrollable(w, false); + lv_obj_set_clickable(w, false); + dr.ballObj[b] = w; + } + lv_obj_set_pos(dr.ballObj[b], (lv_coord_t)(lx - ballR), (lv_coord_t)(ly - ballR)); + lv_obj_set_hidden(dr.ballObj[b], false); + break; + } + } + } +} + +// --------------------------------------------------------------------------- +// Control callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbModeToggle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& dr = mc->drop_; + dr.editMode = !dr.editMode; + // Button label shows what pressing again WILL do (toggle label) + if (dr.modeLbl) lv_label_set_text(dr.modeLbl, dr.editMode ? "DROP" : "EDIT"); + if (dr.modeBtn) { + lv_obj_set_style_bg_color(dr.modeBtn, + dr.editMode ? lv_color_hex(0x008833) : lv_color_hex(0x003388), 0); + } + if (dr.canvas) lv_obj_invalidate(dr.canvas); +} + +static void cbClear(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& dr = mc->drop_; + // Clear balls + for (int b = 0; b < MidiController::DropState::MAX_BALLS; b++) + dr.balls[b].active = false; + // Clear platform labels + for (int p = 0; p < MidiController::DropState::MAX_PLATFORMS; p++) { + if (dr.platLbl[p]) { + lv_obj_delete(dr.platLbl[p]); + dr.platLbl[p] = nullptr; + } + } + dr.numPlatforms = 0; + mc->stopAllNotes(); + if (dr.canvas) lv_obj_invalidate(dr.canvas); +} + +static void cbScaleCycle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->drop_.scale = (mc->drop_.scale + 1) % NUM_MIDI_SCALES; + rebuildPlatformNotes(mc); + if (mc->drop_.statusLbl) lv_label_set_text(mc->drop_.statusLbl, MIDI_SCALES[mc->drop_.scale].name); +} + +static void cbKeyDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->drop_.key = (mc->drop_.key + 11) % 12; + rebuildPlatformNotes(mc); +} +static void cbKeyUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->drop_.key = (mc->drop_.key + 1) % 12; + rebuildPlatformNotes(mc); +} + +static void cbOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->drop_.octave > 2) mc->drop_.octave--; + rebuildPlatformNotes(mc); + if (mc->drop_.octLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "Oct %d", mc->drop_.octave); + lv_label_set_text(mc->drop_.octLbl, buf); + } +} +static void cbOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->drop_.octave < 7) mc->drop_.octave++; + rebuildPlatformNotes(mc); + if (mc->drop_.octLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "Oct %d", mc->drop_.octave); + lv_label_set_text(mc->drop_.octLbl, buf); + } +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildDropView(MidiController* mc, lv_obj_t* parent) { + auto& dr = mc->drop_; + + // Reset volatile state on view entry. ballObj[]/platLbl[] were nulled by + // switchToMode (previous root destroyed), null again here to be safe. + dr.numPlatforms = 0; + dr.editMode = false; + for (auto& ball : dr.balls) ball.active = false; + for (auto& obj : dr.ballObj) obj = nullptr; + for (auto& lbl : dr.platLbl) lbl = nullptr; + + // Clamp octave to valid range [2,7] + dr.octave = std::clamp(dr.octave, 2, 7); + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + int ctrlH = 88; + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // -- Control row (TOP) ---------------------------------------------------- + lv_obj_t* ctrl = mcRow(root, ctrlH); + + auto makeBtn = [&](const char* lbl, int w, lv_event_cb_t cb) -> lv_obj_t* { + return mcSmallBtn(ctrl, lbl, w, cb, mc); + }; + + // < (back) + mcBackBtn(ctrl, cbBack, mc, 96); + + // MODE toggle: DROP / EDIT + dr.modeBtn = makeBtn("EDIT", 176, cbModeToggle); + lv_obj_set_style_bg_color(dr.modeBtn, lv_color_hex(0x003388), 0); + dr.modeLbl = lv_obj_get_child(dr.modeBtn, 0); + + // OCT- label OCT+ + makeBtn("OCT-", 96, cbOctDown); + char octBuf[8]; snprintf(octBuf, sizeof(octBuf), "Oct %d", dr.octave); + dr.octLbl = lv_label_create(ctrl); + lv_label_set_text(dr.octLbl, octBuf); + lv_obj_set_style_text_font(dr.octLbl, fontSmall, 0); + lv_obj_set_style_text_color(dr.octLbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_width(dr.octLbl, 64); + lv_obj_set_style_text_align(dr.octLbl, LV_TEXT_ALIGN_CENTER, 0); + makeBtn("OCT+", 96, cbOctUp); + + // CLR (clears balls and platforms) + makeBtn("CLEAR", 128, cbClear); + + // SCALE | KEY- | KEY+ + lv_obj_t* scaleBtn = mcButton(ctrl); + lv_obj_set_size(scaleBtn, 208, MC_BTN_H); + lv_obj_add_event_cb(scaleBtn, cbScaleCycle, LV_EVENT_CLICKED, mc); + dr.statusLbl = lv_label_create(scaleBtn); + lv_label_set_text(dr.statusLbl, MIDI_SCALES[dr.scale].name); + lv_obj_set_style_text_font(dr.statusLbl, fontSmall, 0); + lv_obj_center(dr.statusLbl); + + makeBtn("KEY-", 96, cbKeyDown); + makeBtn("KEY+", 96, cbKeyUp); + + // -- Physics area (fills remaining space below controls) ------------------- + lv_obj_t* physArea = mcContainer(root); + lv_obj_set_width(physArea, LV_PCT(100)); + lv_obj_set_flex_grow(physArea, 1); + lv_obj_set_style_bg_color(physArea, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(physArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(physArea, 0, 0); + lv_obj_set_style_pad_all(physArea, 0, 0); + lv_obj_set_scrollable(physArea, false); + lv_obj_set_clickable(physArea, true); + lv_obj_add_event_cb(physArea, cbDropDraw, LV_EVENT_DRAW_MAIN, mc); + lv_obj_add_event_cb(physArea, cbCanvasSize, LV_EVENT_SIZE_CHANGED, mc); + lv_obj_add_event_cb(physArea, cbCanvasTouch, LV_EVENT_CLICKED, mc); + dr.canvas = physArea; + + // Start timer + mc->dropTimer_ = lv_timer_create(cbDropTimer, 16, mc); +} diff --git a/Apps/MidiController/main/Source/ViewEuclid.cpp b/Apps/MidiController/main/Source/ViewEuclid.cpp new file mode 100644 index 0000000..341b718 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewEuclid.cpp @@ -0,0 +1,677 @@ +#include "MidiController.h" +#include "LcdDisplay.h" + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// EUCLID view - 8-voice Euclidean rhythm sequencer +// +// (toolbar ~40px, statusbar ~20px → content ~1280×660) +// +// Layout (flex column, fills content area): +// TOP ROW (88px, full width): < | PLAY | BPM- N BPM+ | TRIP ON/OFF +// BODY (flex_grow, flex row): +// LEFT (flex_grow): canvas - 8 concentric rings + sweep line +// RIGHT (500px): controls stacked in flex column +// ├ voice tab row: 8 equal tabs +// ├ voice name label (large, colored) +// └ param rows (flex_grow each): Steps / Events / Rotation / Note +// --------------------------------------------------------------------------- + +// Fixed pixel constants +static constexpr int ROW_H = 88; // matches ViewKeys rowH +static constexpr int COL_W = 500; +static constexpr int CHROME_H = 60; // toolbar (~40px) + statusbar (~20px) +static constexpr int BTN_H = 64; // matches ViewKeys btnH (rowH-24) +static constexpr int BTN_W_SM = 88; // small button (< >) +static constexpr int BTN_W_MD = 144; // medium button (PLAY) +static constexpr int BTN_W_LG = 88; // back button +static constexpr int LBL_W = 100; // value label width +static constexpr int PAD = 12; // row padding / gap - matches ViewKeys + +// Per-voice accent colors (8 voices) +static constexpr uint32_t VOICE_COLORS[8] = { + 0xFF4040, 0xFF9900, 0x44DD44, 0x44BBFF, + 0xFF44FF, 0xFFDD00, 0x00FFCC, 0xFF8888 +}; + +// --------------------------------------------------------------------------- +// Bucket (Bresenham-style) Euclidean pattern generator +// --------------------------------------------------------------------------- + +static void euclidGenPattern(MidiController::EuclidState::Voice& v) { + memset(v.pattern, 0, sizeof(v.pattern)); + if (v.events == 0 || v.steps == 0) return; + int events = std::min(v.events, v.steps); + // Pre-fill the accumulator so the first event lands on step 0. Starting at + // zero instead delays the first event and produces uneven gaps for some + // pairs (for example E(3,8) became 00100110 rather than 10010010). + int bucket = v.steps - events; + for (int i = 0; i < v.steps; i++) { + bucket += events; + if (bucket >= v.steps) { + bucket -= v.steps; + int pos = (i + v.rotation) % v.steps; + if (pos < 0) pos += v.steps; + v.pattern[pos] = true; + } + } +} + +// --------------------------------------------------------------------------- +// Canvas draw +// --------------------------------------------------------------------------- + +// Draw the rings directly in the canvas draw event. Creating and deleting a +// widget for every dot on every step is far too expensive for the LVGL task +// (hundreds of objects per beat) and can starve the idle task. +static void cbEuclidCanvasDraw(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& es = mc->euclid_; + + lv_obj_t* obj = lv_event_get_target_obj(e); + lv_layer_t* layer = lv_event_get_layer(e); + + lv_area_t area; + lv_obj_get_coords(obj, &area); + int32_t ox = area.x1; + int32_t oy = area.y1; + int W = lv_area_get_width(&area); + int H = lv_area_get_height(&area); + if (W <= 4 || H <= 4) return; + + es.canvasW = W; + es.canvasH = H; + + // Background fill + lv_draw_fill_dsc_t bg; + lv_draw_fill_dsc_init(&bg); + bg.color = lv_color_hex(0x040610); + bg.opa = LV_OPA_COVER; + lv_draw_fill(layer, &bg, &area); + + int cx = W / 2; + int cy = H / 2; + int maxR = std::min(cx, cy) - 8; + if (maxR < 20) return; + int ringSpacing = maxR / 9; // 8 rings + small gap from center + + lv_draw_rect_dsc_t dot; + lv_draw_rect_dsc_init(&dot); + dot.radius = LV_RADIUS_CIRCLE; + dot.bg_opa = LV_OPA_COVER; + dot.border_width = 0; + + lv_draw_rect_dsc_t ring; + lv_draw_rect_dsc_init(&ring); + ring.radius = LV_RADIUS_CIRCLE; + ring.bg_opa = LV_OPA_TRANSP; + ring.border_width = 2; + ring.border_opa = LV_OPA_50; + + for (int vi = 0; vi < 8; vi++) { + const auto& v = es.voice[vi]; + if (v.steps == 0) continue; + int r = maxR - vi * ringSpacing; + if (r < 8) continue; + + // Selected voice: subtle ring outline behind the dots + if (vi == es.selectedVoice) { + lv_area_t ringArea = { + (lv_coord_t)(ox + cx - r - 4), + (lv_coord_t)(oy + cy - r - 4), + (lv_coord_t)(ox + cx + r + 4), + (lv_coord_t)(oy + cy + r + 4), + }; + ring.border_color = lv_color_hex(VOICE_COLORS[vi]); + lv_draw_rect(layer, &ring, &ringArea); + } + + for (int step = 0; step < v.steps; step++) { + float angle = (float)(2.0 * M_PI * step) / v.steps - (float)(M_PI / 2.0); + int px = cx + (int)(cosf(angle) * r); + int py = cy + (int)(sinf(angle) * r); + + bool active = v.pattern[step]; + bool isCurrent = (es.playing && step == (es.currentStep % v.steps)); + + uint32_t col; + if (isCurrent && active) col = 0xFFFFFF; + else if (isCurrent) col = 0x44BBFF; + else if (active) col = VOICE_COLORS[vi]; + else col = 0x1A2030; + + int dotR = active ? 7 : 4; + lv_area_t dotArea = { + (lv_coord_t)(ox + px - dotR), + (lv_coord_t)(oy + py - dotR), + (lv_coord_t)(ox + px + dotR - 1), + (lv_coord_t)(oy + py + dotR - 1), + }; + dot.bg_color = lv_color_hex(col); + lv_draw_rect(layer, &dot, &dotArea); + } + } + + // Sweep line - radial from center to edge + if (es.playing && es.voice[0].steps > 0) { + int steps = es.voice[0].steps; + float angle = (float)(2.0 * M_PI * (es.currentStep % steps)) / steps - (float)(M_PI / 2.0); + int ex = cx + (int)(cosf(angle) * maxR); + int ey = cy + (int)(sinf(angle) * maxR); + + lv_draw_line_dsc_t line; + lv_draw_line_dsc_init(&line); + line.color = lv_color_hex(0x2244AA); + line.opa = LV_OPA_60; + line.width = 2; + line.p1.x = (lv_value_precise_t)(ox + cx); + line.p1.y = (lv_value_precise_t)(oy + cy); + line.p2.x = (lv_value_precise_t)(ox + ex); + line.p2.y = (lv_value_precise_t)(oy + ey); + lv_draw_line(layer, &line); + } +} + +static void euclidDrawCanvas(MidiController::EuclidState& es) { + if (es.canvas) lv_obj_invalidate(es.canvas); +} + +// --------------------------------------------------------------------------- +// Update param labels for selected voice +// --------------------------------------------------------------------------- + +static void euclidUpdateParamLabels(MidiController::EuclidState& es) { + const auto& v = es.voice[es.selectedVoice]; + if (es.stepsLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "S:%d", v.steps); + lv_label_set_text(es.stepsLbl, buf); + } + if (es.eventsLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "E:%d", v.events); + lv_label_set_text(es.eventsLbl, buf); + } + if (es.rotLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "R:%d", v.rotation); + lv_label_set_text(es.rotLbl, buf); + } + if (es.noteLbl) { + char buf[8]; midiNoteName(v.note, buf, sizeof(buf)); + lv_label_set_text(es.noteLbl, buf); + } + if (es.voiceLbl) { + char buf[24]; snprintf(buf, sizeof(buf), "Voice %d", es.selectedVoice + 1); + lv_label_set_text(es.voiceLbl, buf); + lv_obj_set_style_text_color(es.voiceLbl, lv_color_hex(VOICE_COLORS[es.selectedVoice]), 0); + } +} + +// --------------------------------------------------------------------------- +// Called by EncoderInput after modifying voice params via knob/button +// --------------------------------------------------------------------------- + +void euclidOnEncoderUpdate(MidiController* mc) { + auto& es = mc->euclid_; + // Regenerate pattern and refresh UI for all 8 voices (knobs may have + // changed any of them, and the canvas shows all voices simultaneously) + for (int i = 0; i < 8; i++) { + euclidGenPattern(es.voice[i]); + } + euclidUpdateParamLabels(es); + euclidDrawCanvas(es); + // Update voice tab button opacities (selected = full, others = dim) + if (es.voiceTabRow) { + uint32_t childCnt = lv_obj_get_child_count(es.voiceTabRow); + for (uint32_t i = 0; i < childCnt && i < 8; i++) { + lv_obj_t* t = lv_obj_get_child(es.voiceTabRow, i); + lv_obj_set_style_bg_opa(t, (i == (uint32_t)es.selectedVoice) ? LV_OPA_COVER : LV_OPA_30, 0); + } + } +} + +// --------------------------------------------------------------------------- +// Timer callback +// --------------------------------------------------------------------------- + +static void cbEuclidTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + auto& es = mc->euclid_; + if (!es.playing) return; + + uint32_t now = lv_tick_get(); + if (now < es.nextStepMs) return; + + mc->sendMidi(0xB0, 123, 0); // all-notes-off + + for (int vi = 0; vi < 8; vi++) { + auto& v = es.voice[vi]; + if (v.steps == 0) continue; + int stepIdx = es.currentStep % v.steps; + if (v.pattern[stepIdx]) { + mc->sendMidi(0x90, (uint8_t)v.note, 110); + } + } + + // Free-running clock: each voice takes currentStep modulo its own length. + // Wrapping at 64 would restart voices whose step count does not divide 64. + es.currentStep++; + + uint32_t stepMs = 60000u / (uint32_t)es.bpm / 4u; + if (es.triplet) stepMs = stepMs * 2 / 3; + if (stepMs < 20) stepMs = 20; + es.nextStepMs = now + stepMs; + + if (es.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", es.bpm); + lv_label_set_text(es.bpmLbl, buf); + } + + euclidDrawCanvas(es); +} + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbEuclidBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbEuclidPlay(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& es = mc->euclid_; + es.playing = !es.playing; + if (!es.playing) { + mc->sendMidi(0xB0, 123, 0); + es.currentStep = 0; + } else { + es.nextStepMs = lv_tick_get(); + } + if (es.playLbl) lv_label_set_text(es.playLbl, es.playing ? "STOP" : "PLAY"); + // Stopping resets the playhead; repaint the rings so it is not left on the + // previous step until the next timer tick (which no longer runs when stopped). + euclidDrawCanvas(es); + lcdUpdate(mc); +} + +static void cbEuclidTrip(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& es = mc->euclid_; + es.triplet = !es.triplet; + if (es.tripLbl) lv_label_set_text(es.tripLbl, es.triplet ? "TRIP ON" : "TRIP OFF"); +} + +static void cbEuclidBpmDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& es = mc->euclid_; + if (es.bpm > 40) es.bpm -= 5; + if (es.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", es.bpm); + lv_label_set_text(es.bpmLbl, buf); + } +} +static void cbEuclidBpmUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& es = mc->euclid_; + if (es.bpm < 280) es.bpm += 5; + if (es.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", es.bpm); + lv_label_set_text(es.bpmLbl, buf); + } +} + +// Voice tab - mc is stored as object user_data, voice index as event user_data +static void cbEuclidVoiceTab(lv_event_t* e) { + lv_obj_t* tab = lv_event_get_target_obj(e); + auto* mc = static_cast(lv_obj_get_user_data(tab)); + if (!mc) return; + auto& es = mc->euclid_; + intptr_t idx = (intptr_t)lv_event_get_user_data(e); + es.selectedVoice = (int)idx; + + // Update tab button opacities: selected = full, others = dim + lv_obj_t* tabRow = lv_obj_get_parent(tab); + if (tabRow) { + uint32_t childCnt = lv_obj_get_child_count(tabRow); + for (uint32_t i = 0; i < childCnt && i < 8; i++) { + lv_obj_t* t = lv_obj_get_child(tabRow, i); + lv_obj_set_style_bg_opa(t, (i == (uint32_t)idx) ? LV_OPA_COVER : LV_OPA_30, 0); + } + } + + euclidUpdateParamLabels(es); + euclidDrawCanvas(es); + lcdUpdate(mc); +} + +static void cbEuclidStepsDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& v = mc->euclid_.voice[mc->euclid_.selectedVoice]; + if (v.steps > 1) v.steps--; + if (v.events > v.steps) v.events = v.steps; + euclidGenPattern(v); + euclidUpdateParamLabels(mc->euclid_); + euclidDrawCanvas(mc->euclid_); +} +static void cbEuclidStepsUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& v = mc->euclid_.voice[mc->euclid_.selectedVoice]; + if (v.steps < 32) v.steps++; + euclidGenPattern(v); + euclidUpdateParamLabels(mc->euclid_); + euclidDrawCanvas(mc->euclid_); +} + +static void cbEuclidEvtsDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& v = mc->euclid_.voice[mc->euclid_.selectedVoice]; + if (v.events > 0) v.events--; + euclidGenPattern(v); + euclidUpdateParamLabels(mc->euclid_); + euclidDrawCanvas(mc->euclid_); +} +static void cbEuclidEvtsUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& v = mc->euclid_.voice[mc->euclid_.selectedVoice]; + if (v.events < v.steps) v.events++; + euclidGenPattern(v); + euclidUpdateParamLabels(mc->euclid_); + euclidDrawCanvas(mc->euclid_); +} + +static void cbEuclidRotDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& v = mc->euclid_.voice[mc->euclid_.selectedVoice]; + v.rotation = (v.rotation - 1 + v.steps) % std::max(1, v.steps); + euclidGenPattern(v); + euclidUpdateParamLabels(mc->euclid_); + euclidDrawCanvas(mc->euclid_); +} +static void cbEuclidRotUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& v = mc->euclid_.voice[mc->euclid_.selectedVoice]; + v.rotation = (v.rotation + 1) % std::max(1, v.steps); + euclidGenPattern(v); + euclidUpdateParamLabels(mc->euclid_); + euclidDrawCanvas(mc->euclid_); +} + +static void cbEuclidNoteDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& v = mc->euclid_.voice[mc->euclid_.selectedVoice]; + if (v.note > 0) v.note--; + euclidUpdateParamLabels(mc->euclid_); +} +static void cbEuclidNoteUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& v = mc->euclid_.voice[mc->euclid_.selectedVoice]; + if (v.note < 127) v.note++; + euclidUpdateParamLabels(mc->euclid_); +} + +// --------------------------------------------------------------------------- +// Build +// --------------------------------------------------------------------------- + +void buildEuclidView(MidiController* mc, lv_obj_t* parent) { + auto& es = mc->euclid_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + const lv_font_t* fontLarge = lvgl_get_text_font(FONT_SIZE_LARGE); + + // Default voice params if not yet set + static const int DEFAULT_STEPS[8] = {16, 16, 16, 16, 16, 16, 16, 16}; + static const int DEFAULT_EVENTS[8] = {4, 4, 8, 5, 3, 6, 7, 2}; + static const int DEFAULT_ROT[8] = {0, 2, 0, 1, 0, 1, 3, 2}; + static const int DEFAULT_NOTE[8] = {36, 38, 42, 39, 45, 47, 49, 51}; + for (int i = 0; i < 8; i++) { + auto& v = es.voice[i]; + if (v.steps == 0) { + v.steps = DEFAULT_STEPS[i]; + v.events = DEFAULT_EVENTS[i]; + v.rotation = DEFAULT_ROT[i]; + v.note = DEFAULT_NOTE[i]; + } + euclidGenPattern(v); + } + + // -- Root: flex COLUMN - full content area -------------------------------- + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_bg_color(root, MC_BG(), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // Shared helpers (used in both transport row and right column) + auto makeBtn = [&](lv_obj_t* par, const char* txt, int w, + lv_event_cb_t cb, void* ud) -> lv_obj_t* { + return mcSmallBtn(par, txt, w, cb, ud); + }; + + auto makeLbl = [&](lv_obj_t* par, const char* txt, int w, + lv_color_t col, const lv_font_t* font = nullptr) -> lv_obj_t* { + lv_obj_t* lbl = lv_label_create(par); + lv_label_set_text(lbl, txt); + lv_obj_set_style_text_font(lbl, font ? font : fontSmall, 0); + lv_obj_set_style_text_color(lbl, col, 0); + lv_obj_set_width(lbl, w); + lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0); + return lbl; + }; + + // -- Row 1: Full-width transport (ROW_H) ---------------------------------- + // < | PLAY | BPM- N BPM+ | TRIP ON/OFF + { + lv_obj_t* topRow = mcRow(root); + + mcBackBtn(topRow, cbEuclidBack, mc, BTN_W_LG); + + lv_obj_t* title = lv_label_create(topRow); + lv_label_set_text(title, "EUCLID"); + lv_obj_set_style_text_font(title, fontNormal, 0); + lv_obj_set_style_text_color(title, lv_color_hex(0x44BBFF), 0); + lv_obj_set_flex_grow(title, 1); + + lv_obj_t* playBtn = makeBtn(topRow, es.playing ? "STOP" : "PLAY", + BTN_W_MD, cbEuclidPlay, mc); + es.playLbl = lv_obj_get_child(playBtn, 0); + + lv_obj_t* sep = lv_label_create(topRow); + lv_label_set_text(sep, "|"); + lv_obj_set_style_text_font(sep, fontSmall, 0); + lv_obj_set_style_text_color(sep, lv_color_hex(0x334455), 0); + + makeBtn(topRow, "<", BTN_W_SM, cbEuclidBpmDown, mc); + char bpmBuf[8]; snprintf(bpmBuf, sizeof(bpmBuf), "%d", es.bpm); + es.bpmLbl = makeLbl(topRow, bpmBuf, LBL_W, lv_color_hex(0xFFE000)); + makeBtn(topRow, ">", BTN_W_SM, cbEuclidBpmUp, mc); + + sep = lv_label_create(topRow); + lv_label_set_text(sep, "|"); + lv_obj_set_style_text_font(sep, fontSmall, 0); + lv_obj_set_style_text_color(sep, lv_color_hex(0x334455), 0); + + lv_obj_t* tripBtn = makeBtn(topRow, es.triplet ? "TRIP ON" : "TRIP OFF", + 120, cbEuclidTrip, mc); + es.tripLbl = lv_obj_get_child(tripBtn, 0); + } + + // -- Body: flex ROW (flex_grow) - canvas LEFT + controls RIGHT ------------ + { + lv_obj_t* body = mcContainer(root); + lv_obj_set_size(body, LV_PCT(100), 0); + lv_obj_set_flex_grow(body, 1); + lv_obj_set_flex_flow(body, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(body, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_bg_opa(body, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(body, 0, 0); + lv_obj_set_style_pad_all(body, 0, 0); + lv_obj_set_style_pad_gap(body, 0, 0); + lv_obj_set_scrollable(body, false); + + // -- LEFT: canvas area (flex_grow, LV_PCT(100) height fills body) ---- + { + lv_obj_t* canvasArea = mcContainer(body); + lv_obj_set_flex_grow(canvasArea, 1); + lv_obj_set_height(canvasArea, LV_PCT(100)); + lv_obj_set_style_bg_color(canvasArea, lv_color_hex(0x040610), 0); + lv_obj_set_style_bg_opa(canvasArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(canvasArea, 0, 0); + lv_obj_set_style_pad_all(canvasArea, 0, 0); + lv_obj_set_scrollable(canvasArea, false); + lv_obj_set_layout(canvasArea, LV_LAYOUT_NONE); + lv_obj_add_event_cb(canvasArea, cbEuclidCanvasDraw, LV_EVENT_DRAW_MAIN, mc); + es.canvas = canvasArea; + // Estimates for first draw before layout settles + es.canvasW = lv_display_get_horizontal_resolution(nullptr) - COL_W; + es.canvasH = lv_display_get_vertical_resolution(nullptr) - CHROME_H - ROW_H; + } + + // -- RIGHT: controls column (500px wide) ------------------------------ + { + + lv_obj_t* rightCol = mcContainer(body); + lv_obj_set_size(rightCol, COL_W, LV_PCT(100)); + lv_obj_set_flex_flow(rightCol, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(rightCol, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_bg_color(rightCol, MC_PANEL(), 0); + lv_obj_set_style_bg_opa(rightCol, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(rightCol, 0, 0); + lv_obj_set_style_pad_all(rightCol, PAD, 0); + lv_obj_set_style_pad_gap(rightCol, PAD, 0); + lv_obj_set_scrollable(rightCol, false); + + auto makeRow = [&](int h) -> lv_obj_t* { + lv_obj_t* row = mcContainer(rightCol); + lv_obj_set_size(row, LV_PCT(100), h); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 0, 0); + lv_obj_set_style_pad_gap(row, PAD, 0); + lv_obj_set_scrollable(row, false); + return row; + }; + + // -- Voice tabs (8 equal, full width) ---------------------------- + { + lv_obj_t* row = makeRow(ROW_H); + es.voiceTabRow = row; + int tabW = (COL_W - PAD * 2 - PAD * 7) / 8; + for (int i = 0; i < 8; i++) { + lv_obj_t* tab = mcButton(row); + lv_obj_set_size(tab, tabW, BTN_H); + lv_obj_set_user_data(tab, mc); + lv_obj_add_event_cb(tab, cbEuclidVoiceTab, LV_EVENT_CLICKED, (void*)(intptr_t)i); + lv_obj_set_style_bg_color(tab, lv_color_hex(VOICE_COLORS[i]), 0); + lv_obj_set_style_bg_opa(tab, (i == es.selectedVoice) ? LV_OPA_COVER : LV_OPA_30, 0); + lv_obj_t* lbl = lv_label_create(tab); + char vbuf[4]; snprintf(vbuf, sizeof(vbuf), "%d", i + 1); + lv_label_set_text(lbl, vbuf); + lv_obj_set_style_text_font(lbl, fontSmall, 0); + lv_obj_center(lbl); + } + } + + // -- Voice name (large, colored) ---------------------------------- + { + lv_obj_t* row = makeRow(ROW_H); + char vbuf[24]; snprintf(vbuf, sizeof(vbuf), "Voice %d", es.selectedVoice + 1); + es.voiceLbl = makeLbl(row, vbuf, COL_W - PAD * 2, + lv_color_hex(VOICE_COLORS[es.selectedVoice]), fontLarge); + } + + // Helper: param row with colored header + < value > buttons + // flex_grow=1 so all 4 rows share remaining space equally + auto makeParamRow = [&](const char* name, lv_color_t col) -> lv_obj_t* { + lv_obj_t* row = mcContainer(rightCol); + lv_obj_set_width(row, LV_PCT(100)); + lv_obj_set_flex_grow(row, 1); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(row, lv_color_hex(0x0C0E1A), 0); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(row, 1, 0); + lv_obj_set_style_border_color(row, lv_color_hex(0x1A2030), 0); + lv_obj_set_style_pad_all(row, PAD, 0); + lv_obj_set_style_pad_gap(row, PAD, 0); + lv_obj_set_scrollable(row, false); + lv_obj_t* hdr = lv_label_create(row); + lv_label_set_text(hdr, name); + lv_obj_set_style_text_font(hdr, fontSmall, 0); + lv_obj_set_style_text_color(hdr, col, 0); + lv_obj_set_width(hdr, 120); + return row; + }; + + // -- Steps -------------------------------------------------------- + { + lv_obj_t* row = makeParamRow("Steps", lv_color_hex(0xFFCC88)); + makeBtn(row, "<", BTN_W_SM, cbEuclidStepsDown, mc); + char sBuf[8]; snprintf(sBuf, sizeof(sBuf), "S:%d", es.voice[es.selectedVoice].steps); + es.stepsLbl = makeLbl(row, sBuf, LBL_W, lv_color_hex(0xFFCC88)); + makeBtn(row, ">", BTN_W_SM, cbEuclidStepsUp, mc); + } + + // -- Events ------------------------------------------------------- + { + lv_obj_t* row = makeParamRow("Events", lv_color_hex(0x88DDFF)); + makeBtn(row, "<", BTN_W_SM, cbEuclidEvtsDown, mc); + char eBuf[8]; snprintf(eBuf, sizeof(eBuf), "E:%d", es.voice[es.selectedVoice].events); + es.eventsLbl = makeLbl(row, eBuf, LBL_W, lv_color_hex(0x88DDFF)); + makeBtn(row, ">", BTN_W_SM, cbEuclidEvtsUp, mc); + } + + // -- Rotation ----------------------------------------------------- + { + lv_obj_t* row = makeParamRow("Rotation", lv_color_hex(0xAAFF88)); + makeBtn(row, "<", BTN_W_SM, cbEuclidRotDown, mc); + char rBuf[8]; snprintf(rBuf, sizeof(rBuf), "R:%d", es.voice[es.selectedVoice].rotation); + es.rotLbl = makeLbl(row, rBuf, LBL_W, lv_color_hex(0xAAFF88)); + makeBtn(row, ">", BTN_W_SM, cbEuclidRotUp, mc); + } + + // -- Note --------------------------------------------------------- + { + lv_obj_t* row = makeParamRow("Note", lv_color_hex(0xFF88CC)); + makeBtn(row, "<", BTN_W_SM, cbEuclidNoteDown, mc); + char nBuf[8]; midiNoteName(es.voice[es.selectedVoice].note, nBuf, sizeof(nBuf)); + es.noteLbl = makeLbl(row, nBuf, LBL_W, lv_color_hex(0xFF88CC)); + makeBtn(row, ">", BTN_W_SM, cbEuclidNoteUp, mc); + } + } + } + + // -- Start timer ---------------------------------------------------------- + mc->euclidTimer_ = lv_timer_create(cbEuclidTimer, 10, mc); + es.nextStepMs = lv_tick_get(); + euclidDrawCanvas(es); +} diff --git a/Apps/MidiController/main/Source/ViewGrid.cpp b/Apps/MidiController/main/Source/ViewGrid.cpp new file mode 100644 index 0000000..25d8b11 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewGrid.cpp @@ -0,0 +1,289 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// GRID view - Isomorphic 8×5 Linnstrument-style grid +// +// Layout: perfect-4ths tuning - each row is 5 semitones higher, each column +1. +// Base note = (octave + 1) * 12 - at oct=3: baseNote=48 (C3). +// --------------------------------------------------------------------------- + +static inline lv_coord_t dispW() { + return (lv_coord_t)lv_display_get_horizontal_resolution(nullptr); +} +// Whether a semitone class is a "black" key +static bool isBlackKey(int semitone) { + semitone = ((semitone % 12) + 12) % 12; + return (semitone == 1 || semitone == 3 || semitone == 6 || semitone == 8 || semitone == 10); +} + +static lv_color_t cellTextColor(int note) { + int sc12 = note % 12; + if (isBlackKey(sc12)) return lv_color_hex(0x808080); // dark cell - light text + return lv_color_hex(0x000000); // C notes and white keys - dark text +} + +static int gridNote(int row, int col, int baseNote) { + // Row 0 = lowest, row 4 = highest (display top = row 4) + int note = baseNote + (row * 5) + col; + return std::max(0, std::min(127, note)); +} + +static bool isNotePressed(const MidiController::GridState& gs, int note) { + for (int slot = 0; slot < LVGL_POINTER_MAX_SLOTS; slot++) { + if (gs.pressedNote[slot] == note) return true; + } + return false; +} + +static void refreshGridColors(MidiController* mc) { + if (!mc) return; + auto& gs = mc->grid_; + int baseNote = (gs.octave + 1) * 12; + for (int r = 0; r < 5; r++) { + for (int c = 0; c < 8; c++) { + if (!gs.cell[r][c]) continue; + int note = gridNote(r, c, baseNote); + int sc12 = note % 12; + lv_color_t cellColor; + if (isNotePressed(gs, note)) { + cellColor = lv_color_hex(0x00DFFF); // pressed + } else if (sc12 == 0) { + cellColor = lv_color_hex(0x00FFCC); // C notes + } else if (isBlackKey(sc12)) { + cellColor = lv_color_hex(0x1A2A40); // black key + } else { + cellColor = lv_color_hex(0x2A3A50); // white key + } + lv_obj_set_style_bg_color(gs.cell[r][c], cellColor, LV_PART_MAIN); + lv_obj_set_user_data(gs.cell[r][c], (void*)(intptr_t)note); + // Update note name label (child 0) + lv_obj_t* lbl = lv_obj_get_child(gs.cell[r][c], 0); + if (lbl) { + char buf[8]; midiNoteName(note, buf, sizeof(buf)); + lv_label_set_text(lbl, buf); + lv_obj_set_style_text_color(lbl, cellTextColor(note), 0); + } + } + } +} + +static void refreshOctLabel(MidiController* mc) { + if (!mc->grid_.octLbl) return; + char buf[10]; snprintf(buf, sizeof(buf), "Oct %d", mc->grid_.octave); + lv_label_set_text(mc->grid_.octLbl, buf); +} + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbCellPressed(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& gs = mc->grid_; + + lv_indev_t* indev = lv_event_get_indev(e); + if (!indev) return; + int8_t slotIdx = lvgl_pointer_get_slot_index(indev); + if (slotIdx < 0) return; + + lv_obj_t* btn = lv_event_get_target_obj(e); + int note = (int)(intptr_t)lv_obj_get_user_data(btn); + if (gs.pressedNote[slotIdx] >= 0) mc->sendMidi(0x80, (uint8_t)gs.pressedNote[slotIdx], 0); + gs.pressedNote[slotIdx] = note; + mc->sendMidi(0x90, (uint8_t)note, 100); + refreshGridColors(mc); + if (mc->grid_.noteLbl) { + char buf[8]; midiNoteName(note, buf, sizeof(buf)); + lv_label_set_text(mc->grid_.noteLbl, buf); + } +} + +static void cbCellReleased(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& gs = mc->grid_; + + lv_indev_t* indev = lv_event_get_indev(e); + if (!indev) return; + int8_t slotIdx = lvgl_pointer_get_slot_index(indev); + if (slotIdx < 0) return; + + lv_obj_t* btn = lv_event_get_target_obj(e); + int note = (int)(intptr_t)lv_obj_get_user_data(btn); + if (gs.pressedNote[slotIdx] == note) { + mc->sendMidi(0x80, (uint8_t)note, 0); + gs.pressedNote[slotIdx] = -1; + refreshGridColors(mc); + if (mc->grid_.noteLbl) lv_label_set_text(mc->grid_.noteLbl, ""); + } +} + +static void releaseAllGridNotes(MidiController* mc) { + auto& gs = mc->grid_; + for (int slot = 0; slot < LVGL_POINTER_MAX_SLOTS; slot++) { + if (gs.pressedNote[slot] >= 0) { + mc->sendMidi(0x80, (uint8_t)gs.pressedNote[slot], 0); + gs.pressedNote[slot] = -1; + } + } + if (mc->grid_.noteLbl) lv_label_set_text(mc->grid_.noteLbl, ""); +} + +static void cbOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->grid_.octave > 1) { + releaseAllGridNotes(mc); + mc->grid_.octave--; + refreshOctLabel(mc); + refreshGridColors(mc); + } +} + +static void cbOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->grid_.octave < 6) { + releaseAllGridNotes(mc); + mc->grid_.octave++; + refreshOctLabel(mc); + refreshGridColors(mc); + } +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildGridView(MidiController* mc, lv_obj_t* parent) { + if (!mc) return; + auto& gs = mc->grid_; + for (auto& n : gs.pressedNote) n = -1; // Ensure clean state when entering grid view + + lv_coord_t W = dispW(); + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + + int ctrlH = 88; + int btnH = MC_BTN_H; + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // -- Control row (TOP) ---------------------------------------------------- + lv_obj_t* ctrl = mcRow(root, ctrlH); + + mcBackBtn(ctrl, cbBack, mc, MC_W_S); + lv_obj_t* titleLbl = lv_label_create(ctrl); + lv_label_set_text(titleLbl, "GRID KEYS"); + lv_obj_set_style_text_font(titleLbl, fontNormal, 0); + lv_obj_set_style_text_color(titleLbl, lv_color_hex(0x44BBFF), 0); + lv_obj_set_flex_grow(titleLbl, 1); + + lv_obj_t* octDown = mcButton(ctrl); + lv_obj_set_size(octDown, 112, btnH); + lv_obj_add_event_cb(octDown, cbOctDown, LV_EVENT_CLICKED, mc); + lv_obj_t* odl = lv_label_create(octDown); lv_label_set_text(odl, "OCT-"); + lv_obj_set_style_text_font(odl, fontSmall, 0); lv_obj_center(odl); + + gs.octLbl = lv_label_create(ctrl); + refreshOctLabel(mc); + lv_obj_set_style_text_font(gs.octLbl, fontSmall, 0); + lv_obj_set_style_text_color(gs.octLbl, lv_color_hex(0x00DFFF), 0); + lv_obj_set_width(gs.octLbl, 176); + lv_obj_set_style_text_align(gs.octLbl, LV_TEXT_ALIGN_CENTER, 0); + + lv_obj_t* octUp = mcButton(ctrl); + lv_obj_set_size(octUp, 112, btnH); + lv_obj_add_event_cb(octUp, cbOctUp, LV_EVENT_CLICKED, mc); + lv_obj_t* oul = lv_label_create(octUp); lv_label_set_text(oul, "OCT+"); + lv_obj_set_style_text_font(oul, fontSmall, 0); lv_obj_center(oul); + + gs.noteLbl = lv_label_create(ctrl); + lv_label_set_text(gs.noteLbl, ""); + lv_obj_set_style_text_font(gs.noteLbl, fontSmall, 0); + lv_obj_set_style_text_color(gs.noteLbl, lv_color_hex(0x00DFFF), 0); + lv_obj_set_flex_grow(gs.noteLbl, 1); + lv_obj_set_style_text_align(gs.noteLbl, LV_TEXT_ALIGN_RIGHT, 0); + + // -- Grid (5 rows × 8 cols, fills available space) ----------------------- + lv_obj_t* gridArea = mcContainer(root); + lv_obj_set_width(gridArea, LV_PCT(100)); + lv_obj_set_flex_grow(gridArea, 1); + lv_obj_set_style_bg_color(gridArea, lv_color_hex(0x050508), 0); + lv_obj_set_style_bg_opa(gridArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(gridArea, 0, 0); + lv_obj_set_style_pad_top(gridArea, 8, 0); + lv_obj_set_style_pad_bottom(gridArea, 16, 0); + lv_obj_set_style_pad_left(gridArea, 16, 0); + lv_obj_set_style_pad_right(gridArea, 16, 0); + lv_obj_set_style_pad_gap(gridArea, 4, 0); + lv_obj_set_scrollable(gridArea, false); + lv_obj_set_flex_flow(gridArea, LV_FLEX_FLOW_COLUMN); + + int cellW = ((int)W - 16 * 2) / 8; + + // Build rows top to bottom: row 4 (highest) at top, row 0 at bottom + for (int r = 4; r >= 0; r--) { + lv_obj_t* rowObj = mcContainer(gridArea); + lv_obj_set_width(rowObj, LV_PCT(100)); + lv_obj_set_flex_grow(rowObj, 1); + lv_obj_set_flex_flow(rowObj, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(rowObj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_opa(rowObj, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(rowObj, 0, 0); + lv_obj_set_style_pad_all(rowObj, 0, 0); + lv_obj_set_style_pad_gap(rowObj, 4, 0); + lv_obj_set_scrollable(rowObj, false); + + for (int c = 0; c < 8; c++) { + lv_obj_t* btn = mcButton(rowObj); + lv_obj_set_size(btn, cellW - 4, LV_PCT(100)); + lv_obj_set_style_bg_color(btn, lv_color_hex(0x2A3A50), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(btn, 8, 0); + lv_obj_set_style_pad_all(btn, 0, 0); + lv_obj_set_style_border_width(btn, 0, 0); + lv_obj_add_event_cb(btn, cbCellPressed, LV_EVENT_PRESSED, mc); + lv_obj_add_event_cb(btn, cbCellReleased, LV_EVENT_RELEASED, mc); + + // Note label (small, aligned center) + int note = gridNote(r, c, (gs.octave + 1) * 12); + lv_obj_set_user_data(btn, (void*)(intptr_t)note); + { + char buf[8]; midiNoteName(note, buf, sizeof(buf)); + lv_obj_t* lbl = lv_label_create(btn); + lv_label_set_text(lbl, buf); + lv_obj_set_style_text_font(lbl, fontSmall, 0); + lv_obj_set_style_text_color(lbl, cellTextColor(note), 0); + lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_clickable(lbl, false); + } + + gs.cell[r][c] = btn; + } + } + + // Apply initial colors + refreshGridColors(mc); +} diff --git a/Apps/MidiController/main/Source/ViewKeys.cpp b/Apps/MidiController/main/Source/ViewKeys.cpp new file mode 100644 index 0000000..da5d9e6 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewKeys.cpp @@ -0,0 +1,427 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// KEYS view - single-octave chromatic keyboard +// +// Layout (flex column): +// [control row: < | OCT- | octave | OCT+ | SCALE | KEY- | key | KEY+] +// [piano area: 7 white keys with 5 black keys overlaid] +// [status label] +// --------------------------------------------------------------------------- + +static constexpr int WHITE_OFF[7] = { 0, 2, 4, 5, 7, 9, 11 }; +static constexpr int BLACK_OFF[5] = { 1, 3, 6, 8, 10 }; +// Center X of each black key as fraction of white-key-width × 100 +static constexpr int BLACK_FRAC[5] = { 65, 165, 365, 465, 565 }; + +static const char* WHITE_NAME[7] = { "C","D","E","F","G","A","B" }; + +// Key colors by role +static lv_color_t keyColorWhiteDefault() { return lv_color_hex(0xE8E8D4); } +static lv_color_t keyColorBlackDefault() { return lv_color_hex(0x111111); } + +static inline lv_coord_t dispW() { + return (lv_coord_t)lv_display_get_horizontal_resolution(nullptr); +} + +static void resetKeyColors(MidiController* mc) { + auto& ks = mc->keys_; + + for (int i = 0; i < 7; i++) { + if (!ks.whiteKey[i]) continue; + lv_obj_set_style_bg_color(ks.whiteKey[i], keyColorWhiteDefault(), LV_PART_MAIN); + } + + for (int i = 0; i < 5; i++) { + if (!ks.blackKey[i]) continue; + lv_obj_set_style_bg_color(ks.blackKey[i], keyColorBlackDefault(), LV_PART_MAIN); + } +} + +// Rebuild note assignments after an octave change, and refresh the scale/key labels +static void rebuildKeyNotes(MidiController* mc) { + auto& ks = mc->keys_; + + // White keys + for (int k = 0; k < 7; k++) { + if (!ks.whiteKey[k]) continue; + int note = std::max(0, std::min(127, (ks.octave + 1) * 12 + WHITE_OFF[k])); + lv_obj_set_user_data(ks.whiteKey[k], (void*)(intptr_t)note); + + // Update C note label + if (k == 0) { + lv_obj_t* lbl = lv_obj_get_child(ks.whiteKey[k], 0); + if (lbl) { + char buf[16]; snprintf(buf, sizeof(buf), "C%d", ks.octave); + lv_label_set_text(lbl, buf); + } + } + } + + // Black keys + for (int k = 0; k < 5; k++) { + if (!ks.blackKey[k]) continue; + int note = std::max(0, std::min(127, (ks.octave + 1) * 12 + BLACK_OFF[k])); + lv_obj_set_user_data(ks.blackKey[k], (void*)(intptr_t)note); + } + + // Update status label + if (ks.statusLbl) { + char buf[48]; + snprintf(buf, sizeof(buf), "Oct %d | %s | Key %s", + ks.octave, + MIDI_SCALES[ks.scale % NUM_MIDI_SCALES].name, + KEY_NAMES[ks.key % 12]); + lv_label_set_text(ks.statusLbl, buf); + } + if (ks.octLbl) { + char buf[16]; snprintf(buf, sizeof(buf), "Oct %d", ks.octave); + lv_label_set_text(ks.octLbl, buf); + } + if (ks.scaleLbl) { + lv_label_set_text(ks.scaleLbl, MIDI_SCALES[ks.scale % NUM_MIDI_SCALES].name); + } + if (ks.keyLbl) { + lv_label_set_text(ks.keyLbl, KEY_NAMES[ks.key % 12]); + } + + resetKeyColors(mc); +} + +// --------------------------------------------------------------------------- +// Touch handling - drag across keys (pressing on pianoArea) +// --------------------------------------------------------------------------- + +static void cbPianoPressing(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ks = mc->keys_; + + // lv_event_get_indev() (the indev that actually fired THIS event), not lv_indev_active() - + // the latter is a process-global "whichever indev is being processed right now" and would + // make every finger's hit-test collapse onto whichever touch happened to be read last, once + // multiple simultaneous touch slots exist (see lvgl-module's pointer pool). slotIdx indexes + // ks.lastNote so each finger tracks its own held note independently. + lv_indev_t* indev = lv_event_get_indev(e); + if (!indev) return; + int8_t slotIdx = lvgl_pointer_get_slot_index(indev); + if (slotIdx < 0) return; + lv_point_t pt; + lv_indev_get_point(indev, &pt); + + if (!ks.pianoArea) return; + lv_area_t rowArea; + lv_obj_get_coords(ks.pianoArea, &rowArea); + if (pt.y < rowArea.y1 || pt.y > rowArea.y2) return; + + // Black keys take priority - check them first + int newNote = -1; + for (int i = 0; i < 5; i++) { + if (!ks.blackKey[i]) continue; + lv_area_t area; + lv_obj_get_coords(ks.blackKey[i], &area); + if (pt.x >= area.x1 && pt.x <= area.x2 && pt.y >= area.y1 && pt.y <= area.y2) { + newNote = (int)(intptr_t)lv_obj_get_user_data(ks.blackKey[i]); + break; + } + } + + // Then white keys + if (newNote < 0) { + for (int i = 0; i < 7; i++) { + if (!ks.whiteKey[i]) continue; + lv_area_t area; + lv_obj_get_coords(ks.whiteKey[i], &area); + if (pt.x >= area.x1 && pt.x <= area.x2) { + newNote = (int)(intptr_t)lv_obj_get_user_data(ks.whiteKey[i]); + break; + } + } + } + + if (newNote < 0) return; + if (newNote == ks.lastNote[slotIdx]) return; + + // Slide: off old note, on new note (only this finger's note - other slots' held notes are + // untouched) + if (ks.lastNote[slotIdx] >= 0) { + mc->sendMidi(0x80, (uint8_t)ks.lastNote[slotIdx], 0); + } + mc->sendMidi(0x90, (uint8_t)newNote, 100); + ks.lastNote[slotIdx] = newNote; +} + +static void cbPianoReleased(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ks = mc->keys_; + + lv_indev_t* indev = lv_event_get_indev(e); + if (!indev) return; + int8_t slotIdx = lvgl_pointer_get_slot_index(indev); + if (slotIdx < 0) return; + + if (ks.lastNote[slotIdx] >= 0) { + mc->sendMidi(0x80, (uint8_t)ks.lastNote[slotIdx], 0); + ks.lastNote[slotIdx] = -1; + } +} + +// --------------------------------------------------------------------------- +// Control callbacks +// --------------------------------------------------------------------------- + +static void cbOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->keys_.octave > 1) { + mc->keys_.octave--; + rebuildKeyNotes(mc); + } +} + +static void cbOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->keys_.octave < 8) { + mc->keys_.octave++; + rebuildKeyNotes(mc); + } +} + +static void cbScaleCycle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->keys_.scale = (mc->keys_.scale + 1) % NUM_MIDI_SCALES; + rebuildKeyNotes(mc); +} + +static void cbKeyDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->keys_.key = (mc->keys_.key + 11) % 12; + rebuildKeyNotes(mc); +} + +static void cbKeyUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->keys_.key = (mc->keys_.key + 1) % 12; + rebuildKeyNotes(mc); +} + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildKeysView(MidiController* mc, lv_obj_t* parent) { + auto& ks = mc->keys_; + // A note held when the view was left is already silenced by the mode-switch + // All Notes Off; clear the per-slot state so the next tap always sends Note On. + for (auto& n : ks.lastNote) n = -1; + + lv_coord_t W = dispW(); + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + + int rowH = 88; // single combined control row height + int statusH = 56; + + // Root container + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // -- Single control row: [<] [OCT-] [OCT] [OCT+] | [SCALE] [KEY-] [KEY] [KEY+] -- + lv_obj_t* ctrl = mcRow(root, rowH); + + int btnH = MC_BTN_H; + + // Back button + mcBackBtn(ctrl, cbBack, mc, MC_W_S); + + // OCT - + lv_obj_t* octDownBtn = mcButton(ctrl); + lv_obj_set_size(octDownBtn, 144, btnH); + lv_obj_add_event_cb(octDownBtn, cbOctDown, LV_EVENT_CLICKED, mc); + lv_obj_t* odLbl = lv_label_create(octDownBtn); + lv_label_set_text(odLbl, "OCT-"); + lv_obj_set_style_text_font(odLbl, fontSmall, 0); + lv_obj_center(odLbl); + + // OCT label + ks.octLbl = lv_label_create(ctrl); + char obuf[16]; snprintf(obuf, sizeof(obuf), "Oct %d", ks.octave); + lv_label_set_text(ks.octLbl, obuf); + lv_obj_set_style_text_font(ks.octLbl, fontSmall, 0); + lv_obj_set_style_text_color(ks.octLbl, lv_color_hex(0x00DFFF), 0); + lv_obj_set_width(ks.octLbl, 144); + lv_obj_set_style_text_align(ks.octLbl, LV_TEXT_ALIGN_CENTER, 0); + + // OCT + + lv_obj_t* octUpBtn = mcButton(ctrl); + lv_obj_set_size(octUpBtn, 144, btnH); + lv_obj_add_event_cb(octUpBtn, cbOctUp, LV_EVENT_CLICKED, mc); + lv_obj_t* ouLbl = lv_label_create(octUpBtn); + lv_label_set_text(ouLbl, "OCT+"); + lv_obj_set_style_text_font(ouLbl, fontSmall, 0); + lv_obj_center(ouLbl); + + // Divider label + lv_obj_t* div = lv_label_create(ctrl); + lv_label_set_text(div, "|"); + lv_obj_set_style_text_color(div, lv_color_hex(0x334455), 0); + lv_obj_set_style_text_font(div, fontSmall, 0); + + // Scale cycle button + lv_obj_t* scaleBtn = mcButton(ctrl); + lv_obj_set_size(scaleBtn, 224, btnH); + lv_obj_add_event_cb(scaleBtn, cbScaleCycle, LV_EVENT_CLICKED, mc); + ks.scaleLbl = lv_label_create(scaleBtn); + lv_label_set_text(ks.scaleLbl, MIDI_SCALES[ks.scale % NUM_MIDI_SCALES].name); + lv_obj_set_style_text_font(ks.scaleLbl, fontSmall, 0); + lv_obj_center(ks.scaleLbl); + + // KEY - + lv_obj_t* keyDownBtn = mcButton(ctrl); + lv_obj_set_size(keyDownBtn, 112, btnH); + lv_obj_add_event_cb(keyDownBtn, cbKeyDown, LV_EVENT_CLICKED, mc); + lv_obj_t* kdLbl = lv_label_create(keyDownBtn); + lv_label_set_text(kdLbl, "KEY-"); + lv_obj_set_style_text_font(kdLbl, fontSmall, 0); + lv_obj_center(kdLbl); + + // Key label + ks.keyLbl = lv_label_create(ctrl); + lv_label_set_text(ks.keyLbl, KEY_NAMES[ks.key % 12]); + lv_obj_set_style_text_font(ks.keyLbl, fontNormal, 0); + lv_obj_set_style_text_color(ks.keyLbl, MC_VALUE(), 0); + lv_obj_set_width(ks.keyLbl, 96); + lv_obj_set_style_text_align(ks.keyLbl, LV_TEXT_ALIGN_CENTER, 0); + + // KEY + + lv_obj_t* keyUpBtn = mcButton(ctrl); + lv_obj_set_size(keyUpBtn, 112, btnH); + lv_obj_add_event_cb(keyUpBtn, cbKeyUp, LV_EVENT_CLICKED, mc); + lv_obj_t* kuLbl = lv_label_create(keyUpBtn); + lv_label_set_text(kuLbl, "KEY+"); + lv_obj_set_style_text_font(kuLbl, fontSmall, 0); + lv_obj_center(kuLbl); + + // -- Piano area (single row/octave, flex_grow fills remaining height) ----- + lv_obj_t* pianoArea = mcContainer(root); + lv_obj_set_width(pianoArea, LV_PCT(100)); + lv_obj_set_flex_grow(pianoArea, 1); + lv_obj_set_style_bg_color(pianoArea, lv_color_hex(0x050508), 0); + lv_obj_set_style_bg_opa(pianoArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(pianoArea, 0, 0); + lv_obj_set_style_radius(pianoArea, 0, 0); + lv_obj_set_style_pad_all(pianoArea, 0, 0); + lv_obj_set_scrollable(pianoArea, false); + + lv_obj_add_event_cb(pianoArea, cbPianoPressing, LV_EVENT_PRESSING, mc); + lv_obj_add_event_cb(pianoArea, cbPianoReleased, LV_EVENT_RELEASED, mc); + + ks.pianoArea = pianoArea; + + int wkGap = 2; // seam between white keys (dark bg shows through) + int wkw = W / 7; // white key cell width + int bkw = (wkw * 3) / 5; // black key width (~60%) + + // White keys (7), full height, thin dark seam between each - no border stroke + for (int k = 0; k < 7; k++) { + int midiNote = std::max(0, std::min(127, (ks.octave + 1) * 12 + WHITE_OFF[k])); + + lv_obj_t* btn = mcButton(pianoArea); + lv_obj_set_pos(btn, k * wkw, 0); + lv_obj_set_size(btn, wkw - wkGap, LV_PCT(100)); + lv_obj_set_style_bg_color(btn, keyColorWhiteDefault(), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(btn, 0, 0); + lv_obj_set_style_radius(btn, 0, 0); + lv_obj_set_style_pad_all(btn, 0, 0); + lv_obj_set_style_shadow_width(btn, 0, 0); + // Without this, LVGL locks the press onto the first key touched and never + // re-detects the object under the finger while sliding (see lv_indev.c + // indev_proc_press: press-locked objects skip re-search) - breaks piano slide. + lv_obj_set_press_lock(btn, false); + lv_obj_set_user_data(btn, (void*)(intptr_t)midiNote); + // Note On/Off via the row container's PRESSING/RELEASED handlers + // but also handle individual taps via PRESSED/RELEASED on each key + lv_obj_add_event_cb(btn, cbPianoPressing, LV_EVENT_PRESSED, mc); + lv_obj_add_event_cb(btn, cbPianoReleased, LV_EVENT_RELEASED, mc); + + // Note label (C note shows octave, others show letter) + lv_obj_t* lbl = lv_label_create(btn); + if (k == 0) { + char buf[16]; snprintf(buf, sizeof(buf), "C%d", ks.octave); + lv_label_set_text(lbl, buf); + } else { + lv_label_set_text(lbl, WHITE_NAME[k]); + } + lv_obj_set_style_text_font(lbl, fontSmall, 0); + lv_obj_set_style_text_color(lbl, lv_color_hex(0x555566), 0); + lv_obj_align(lbl, LV_ALIGN_BOTTOM_MID, 0, -6); + + ks.whiteKey[k] = btn; + } + + // Black keys (5), shorter, straddling the seam between white keys, on top + for (int k = 0; k < 5; k++) { + int midiNote = std::max(0, std::min(127, (ks.octave + 1) * 12 + BLACK_OFF[k])); + int centerX = (BLACK_FRAC[k] * wkw) / 100; + int leftX = centerX - bkw / 2; + + lv_obj_t* btn = mcButton(pianoArea); + lv_obj_set_pos(btn, leftX, 0); + lv_obj_set_size(btn, bkw, LV_PCT(60)); + lv_obj_set_style_bg_color(btn, keyColorBlackDefault(), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(btn, 0, 0); + lv_obj_set_style_radius(btn, 0, 0); + lv_obj_set_style_pad_all(btn, 0, 0); + lv_obj_set_style_shadow_width(btn, 0, 0); + lv_obj_set_press_lock(btn, false); + lv_obj_set_user_data(btn, (void*)(intptr_t)midiNote); + lv_obj_add_event_cb(btn, cbPianoPressing, LV_EVENT_PRESSED, mc); + lv_obj_add_event_cb(btn, cbPianoReleased, LV_EVENT_RELEASED, mc); + lv_obj_move_foreground(btn); // render on top of white keys + + ks.blackKey[k] = btn; + } + + // -- Status bar ------------------------------------------------------------ + lv_obj_t* statusBar = mcContainer(root); + lv_obj_set_size(statusBar, LV_PCT(100), statusH); + lv_obj_set_style_bg_color(statusBar, lv_color_hex(0x0A0C14), 0); + lv_obj_set_style_bg_opa(statusBar, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(statusBar, 0, 0); + lv_obj_set_style_pad_all(statusBar, 8, 0); + lv_obj_set_scrollable(statusBar, false); + + ks.statusLbl = lv_label_create(statusBar); + lv_obj_set_style_text_font(ks.statusLbl, fontSmall, 0); + lv_obj_set_style_text_color(ks.statusLbl, lv_color_hex(0x666688), 0); + lv_obj_center(ks.statusLbl); + + // Apply initial labels and default key colours + rebuildKeyNotes(mc); +} diff --git a/Apps/MidiController/main/Source/ViewLfo.cpp b/Apps/MidiController/main/Source/ViewLfo.cpp new file mode 100644 index 0000000..1e20de9 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewLfo.cpp @@ -0,0 +1,449 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// LFO view - Low-frequency oscillator for CC/Pitch modulation +// +// Layout (flex column): +// [row 1: < | LFO MOD | status dot] +// [row 2: START/STOP | Rate -/val/Hz/+ | WAVE] +// [row 3: Amt -/val/+ | Tgt CC -/val/+ | PITCH] +// [waveform area (LV_EVENT_DRAW_MAIN)] +// [value readout (flex_grow)] +// --------------------------------------------------------------------------- + +static const char* WAVE_NAMES[] = { "SINE", "TRI", "SQR", "SAW" }; + + +// --------------------------------------------------------------------------- +// Waveform computation +// --------------------------------------------------------------------------- + +static float computeWave(int waveform, float phase) { + switch (waveform) { + case 0: return sinf(phase * (float)(2.0 * M_PI)); + case 1: return (phase < 0.5f) ? (phase * 4.0f - 1.0f) : (3.0f - phase * 4.0f); + case 2: return (phase < 0.5f) ? 1.0f : -1.0f; + case 3: return (2.0f * phase) - 1.0f; + default: return 0.0f; + } +} + +static int waveToCCValue(float wave, int amount, int center = 64) { + int val = center + (int)(wave * (float)amount * 0.5f); + return val < 0 ? 0 : (val > 127 ? 127 : val); +} + +// --------------------------------------------------------------------------- +// Waveform draw event +// --------------------------------------------------------------------------- + +static void cbWaveDraw(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& lf = mc->lfo_; + + lv_obj_t* obj = lv_event_get_target_obj(e); + lv_layer_t* layer = lv_event_get_layer(e); + + lv_area_t area; + lv_obj_get_coords(obj, &area); + int32_t ox = area.x1, oy = area.y1; + int32_t cW = lv_area_get_width(&area); + int32_t cH = lv_area_get_height(&area); + + lf.canvasW = (int)cW; + lf.canvasH = (int)cH; + + // Background + lv_draw_fill_dsc_t bg; + lv_draw_fill_dsc_init(&bg); + bg.color = lv_color_hex(0x080C12); + bg.opa = LV_OPA_COVER; + lv_draw_fill(layer, &bg, &area); + + if (cW <= 1 || cH <= 1) return; + + lv_draw_line_dsc_t line; + lv_draw_line_dsc_init(&line); + line.opa = LV_OPA_COVER; + + // Center line + line.color = lv_color_hex(0x1A2A40); + line.width = 1; + line.p1.x = (lv_value_precise_t)ox; + line.p1.y = (lv_value_precise_t)(oy + cH / 2); + line.p2.x = (lv_value_precise_t)(ox + cW - 1); + line.p2.y = (lv_value_precise_t)(oy + cH / 2); + lv_draw_line(layer, &line); + + // Waveform - initialize prevY from waveform value at phase 0 (not hardcoded cH/2) + line.color = lv_color_hex(0x00DFFF); + line.width = 1; + float v0 = computeWave(lf.waveform, 0.0f); + int prevY = (int)((1.0f - (v0 + 1.0f) * 0.5f) * (float)(cH - 1)); + prevY = prevY < 0 ? 0 : (prevY >= cH ? cH - 1 : prevY); + for (int x = 1; x < cW; x++) { + float ph = (float)x / (float)cW; + float val = computeWave(lf.waveform, ph); + int y = (int)((1.0f - (val + 1.0f) * 0.5f) * (float)(cH - 1)); + y = y < 0 ? 0 : (y >= cH ? cH - 1 : y); + line.p1.x = (lv_value_precise_t)(ox + x - 1); + line.p1.y = (lv_value_precise_t)(oy + prevY); + line.p2.x = (lv_value_precise_t)(ox + x); + line.p2.y = (lv_value_precise_t)(oy + y); + lv_draw_line(layer, &line); + prevY = y; + } +} + +// --------------------------------------------------------------------------- +// LFO timer +// --------------------------------------------------------------------------- + +static void cbLfoTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc || !mc->lfo_.running) return; + auto& lf = mc->lfo_; + + uint64_t nowUs = (uint64_t)esp_timer_get_time(); + uint64_t deltaUs = nowUs - lf.lastUpdateUs; + lf.lastUpdateUs = nowUs; + + float phaseInc = lf.rate * (float)deltaUs / 1000000.0f; + lf.phase = fmodf(lf.phase + phaseInc, 1.0f); + + float wave = computeWave(lf.waveform, lf.phase); + + if (lf.pitchMode) { + int bend = 8192 + (int)(wave * (float)lf.amount * 64.0f); + bend = bend < 0 ? 0 : (bend > 16383 ? 16383 : bend); + uint8_t lsb = bend & 0x7F; + uint8_t msb = (bend >> 7) & 0x7F; + if (lf.lastValue != bend / 128) { + mc->sendMidi(0xE0, lsb, msb); + lf.lastValue = bend / 128; + } + } else { + int ccVal = waveToCCValue(wave, lf.amount); + if (ccVal != lf.lastValue) { + mc->sendMidi(0xB0, (uint8_t)lf.ccTarget, (uint8_t)ccVal); + lf.lastValue = ccVal; + } + } + + lf.updateCounter++; + if (lf.updateCounter >= 5) { + lf.updateCounter = 0; + if (lf.valueLbl) { + char buf[12]; snprintf(buf, sizeof(buf), "Val: %d", lf.lastValue); + lv_label_set_text(lf.valueLbl, buf); + } + if (lf.statusLbl) { + lv_label_set_text(lf.statusLbl, + lf.pitchMode ? "RUN PITCH" : "RUN CC"); + } + } +} + +// --------------------------------------------------------------------------- +// Control callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbStartStop(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& lf = mc->lfo_; + lf.running = !lf.running; + lf.lastUpdateUs = (uint64_t)esp_timer_get_time(); + if (lf.running) { + if (!mc->lfoTimer_) { + mc->lfoTimer_ = lv_timer_create(cbLfoTimer, 20, mc); + } + } else { + if (mc->lfoTimer_) { lv_timer_delete(mc->lfoTimer_); mc->lfoTimer_ = nullptr; } + if (lf.pitchMode) mc->sendMidi(0xE0, 0x00, 0x40); + } + if (lf.startBtn) { + lv_obj_set_style_bg_color(lf.startBtn, + lf.running ? lv_color_hex(0xFF2020) : lv_color_hex(0x00AA44), 0); + } + if (lf.startLbl) lv_label_set_text(lf.startLbl, lf.running ? "STOP" : "START"); + if (lf.statusLbl) lv_label_set_text(lf.statusLbl, lf.running ? "RUN" : "IDLE"); +} + +static void cbRateDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->lfo_.rate -= 0.1f; + if (mc->lfo_.rate < 0.1f) mc->lfo_.rate = 0.1f; + if (mc->lfo_.rateLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%.1f", mc->lfo_.rate); + lv_label_set_text(mc->lfo_.rateLbl, buf); + } +} +static void cbRateUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->lfo_.rate += 0.1f; + if (mc->lfo_.rate > 10.0f) mc->lfo_.rate = 10.0f; + if (mc->lfo_.rateLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%.1f", mc->lfo_.rate); + lv_label_set_text(mc->lfo_.rateLbl, buf); + } +} + +static void cbWaveCycle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->lfo_.waveform = (mc->lfo_.waveform + 1) % 4; + if (mc->lfo_.waveLbl) lv_label_set_text(mc->lfo_.waveLbl, WAVE_NAMES[mc->lfo_.waveform]); + // Invalidate canvas to trigger redraw + if (mc->lfo_.canvas) lv_obj_invalidate(mc->lfo_.canvas); +} + +static void cbAmountDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->lfo_.amount -= 5; if (mc->lfo_.amount < 0) mc->lfo_.amount = 0; + if (mc->lfo_.amountLbl) { + char buf[10]; snprintf(buf, sizeof(buf), "Amt:%d", mc->lfo_.amount); + lv_label_set_text(mc->lfo_.amountLbl, buf); + } +} +static void cbAmountUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->lfo_.amount += 5; if (mc->lfo_.amount > 127) mc->lfo_.amount = 127; + if (mc->lfo_.amountLbl) { + char buf[10]; snprintf(buf, sizeof(buf), "Amt:%d", mc->lfo_.amount); + lv_label_set_text(mc->lfo_.amountLbl, buf); + } +} + +static void cbCCDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc || mc->lfo_.pitchMode) return; + mc->lfo_.ccTarget = (mc->lfo_.ccTarget - 1 + 128) % 128; + if (mc->lfo_.ccLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "CC %d", mc->lfo_.ccTarget); + lv_label_set_text(mc->lfo_.ccLbl, buf); + } +} +static void cbCCUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc || mc->lfo_.pitchMode) return; + mc->lfo_.ccTarget = (mc->lfo_.ccTarget + 1) % 128; + if (mc->lfo_.ccLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "CC %d", mc->lfo_.ccTarget); + lv_label_set_text(mc->lfo_.ccLbl, buf); + } +} + +static void cbPitchToggle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& lf = mc->lfo_; + bool wasPitch = lf.pitchMode; + lf.pitchMode = !lf.pitchMode; + // Leaving pitch mode must release the bend, otherwise the receiver stays + // detuned. Force the next CC update as well so the CC target is refreshed. + if (wasPitch) mc->sendMidi(0xE0, 0x00, 0x40); + lf.lastValue = -1; + if (mc->lfo_.ccLbl) { + if (mc->lfo_.pitchMode) { + lv_label_set_text(mc->lfo_.ccLbl, "PITCH"); + lv_obj_set_style_text_color(mc->lfo_.ccLbl, lv_color_hex(0xFF8000), 0); + } else { + char buf[8]; snprintf(buf, sizeof(buf), "CC %d", mc->lfo_.ccTarget); + lv_label_set_text(mc->lfo_.ccLbl, buf); + lv_obj_set_style_text_color(mc->lfo_.ccLbl, lv_color_hex(0x00DFFF), 0); + } + } +} + +// --------------------------------------------------------------------------- +// Teardown +// --------------------------------------------------------------------------- + +static void cbLfoRootDelete(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& lf = mc->lfo_; + + // Stop and delete the LFO timer so it cannot fire after widgets are gone + if (mc->lfoTimer_) { + lv_timer_delete(mc->lfoTimer_); + mc->lfoTimer_ = nullptr; + } + // Do not leave a pitch-bend receiver detuned when leaving the view. + if (lf.pitchMode) mc->sendMidi(0xE0, 0x00, 0x40); + lf.running = false; + + // Null all stored LVGL pointers + lf.canvas = nullptr; + lf.statusLbl = nullptr; + lf.rateLbl = nullptr; + lf.amountLbl = nullptr; + lf.ccLbl = nullptr; + lf.waveLbl = nullptr; + lf.valueLbl = nullptr; + lf.startBtn = nullptr; + lf.startLbl = nullptr; +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildLfoView(MidiController* mc, lv_obj_t* parent) { + auto& lf = mc->lfo_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + + int rowH = 88; + int waveH = 160; + + lv_obj_t* root = mcContainer(parent); + lv_obj_add_event_cb(root, cbLfoRootDelete, LV_EVENT_DELETE, mc); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 1, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + auto makeRow = [&]() -> lv_obj_t* { return mcRow(root); }; + + auto makeBtn = [&](lv_obj_t* par, const char* txt, int w, lv_event_cb_t cb) -> lv_obj_t* { + return mcSmallBtn(par, txt, w, cb, mc); + }; + + auto makeLbl = [&](lv_obj_t* par, const char* txt, int w, lv_color_t col) -> lv_obj_t* { + return mcValue(par, txt, w, col); + }; + + // -- Row 1: [<] [LFO MOD] [status] ---------------------------------------- + lv_obj_t* row1 = makeRow(); + mcBackBtn(row1, cbBack, mc, MC_W_M); + lv_obj_t* title = lv_label_create(row1); + lv_label_set_text(title, "LFO MOD"); + lv_obj_set_style_text_font(title, fontNormal, 0); + lv_obj_set_style_text_color(title, lv_color_hex(0xCC44FF), 0); + lv_obj_set_flex_grow(title, 1); + lf.statusLbl = lv_label_create(row1); + lv_label_set_text(lf.statusLbl, "IDLE"); + lv_obj_set_style_text_font(lf.statusLbl, fontSmall, 0); + lv_obj_set_style_text_color(lf.statusLbl, MC_LABEL(), 0); + + // -- Row 2: START/STOP | Rate -/val/Hz/+ | WAVE --------------------------- + lv_obj_t* row2 = makeRow(); + lf.startBtn = mcButton(row2); + lv_obj_set_size(lf.startBtn, 224, MC_BTN_H); + lv_obj_set_style_bg_color(lf.startBtn, lv_color_hex(0x00AA44), 0); + lv_obj_add_event_cb(lf.startBtn, cbStartStop, LV_EVENT_CLICKED, mc); + lf.startLbl = lv_label_create(lf.startBtn); + lv_label_set_text(lf.startLbl, "START"); + lv_obj_set_style_text_font(lf.startLbl, fontSmall, 0); + lv_obj_center(lf.startLbl); + + lv_obj_t* rHdr = lv_label_create(row2); + lv_label_set_text(rHdr, " Rate:"); + lv_obj_set_style_text_font(rHdr, fontSmall, 0); + lv_obj_set_style_text_color(rHdr, lv_color_hex(0x666688), 0); + makeBtn(row2, "-", 96, cbRateDown); + char rbuf[8]; snprintf(rbuf, sizeof(rbuf), "%.1f", lf.rate); + lf.rateLbl = makeLbl(row2, rbuf, 144, lv_color_hex(0x00DFFF)); + lv_obj_t* rHz = lv_label_create(row2); + lv_label_set_text(rHz, "Hz"); + lv_obj_set_style_text_font(rHz, fontSmall, 0); + lv_obj_set_style_text_color(rHz, lv_color_hex(0x666688), 0); + makeBtn(row2, "+", 96, cbRateUp); + + lv_obj_t* waveBtn = mcButton(row2); + lv_obj_set_size(waveBtn, 208, MC_BTN_H); + lv_obj_add_event_cb(waveBtn, cbWaveCycle, LV_EVENT_CLICKED, mc); + lf.waveLbl = lv_label_create(waveBtn); + lv_label_set_text(lf.waveLbl, WAVE_NAMES[lf.waveform]); + lv_obj_set_style_text_font(lf.waveLbl, fontSmall, 0); + lv_obj_center(lf.waveLbl); + + // -- Row 3: Amount -/val/+ | CC -/val/+ | PITCH --------------------------- + lv_obj_t* row3 = makeRow(); + + lv_obj_t* aHdr = lv_label_create(row3); + lv_label_set_text(aHdr, "Amt:"); + lv_obj_set_style_text_font(aHdr, fontSmall, 0); + lv_obj_set_style_text_color(aHdr, lv_color_hex(0x666688), 0); + makeBtn(row3, "-", 96, cbAmountDown); + char abuf[10]; snprintf(abuf, sizeof(abuf), "Amt:%d", lf.amount); + lf.amountLbl = makeLbl(row3, abuf, 208, lv_color_hex(0xFFE000)); + makeBtn(row3, "+", 96, cbAmountUp); + + lv_obj_t* cHdr = lv_label_create(row3); + lv_label_set_text(cHdr, " Tgt:"); + lv_obj_set_style_text_font(cHdr, fontSmall, 0); + lv_obj_set_style_text_color(cHdr, lv_color_hex(0x666688), 0); + makeBtn(row3, "-", 96, cbCCDown); + char ccbuf[10]; snprintf(ccbuf, sizeof(ccbuf), "CC %d", lf.ccTarget); + lf.ccLbl = makeLbl(row3, ccbuf, 192, lv_color_hex(0x00DFFF)); + makeBtn(row3, "+", 96, cbCCUp); + + lv_obj_t* pitchBtn = mcButton(row3); + lv_obj_set_size(pitchBtn, 192, MC_BTN_H); + lv_obj_set_style_bg_color(pitchBtn, lv_color_hex(0x220033), 0); + lv_obj_add_event_cb(pitchBtn, cbPitchToggle, LV_EVENT_CLICKED, mc); + lv_obj_t* pitchLbl = lv_label_create(pitchBtn); + lv_label_set_text(pitchLbl, "PITCH"); + lv_obj_set_style_text_font(pitchLbl, fontSmall, 0); + lv_obj_center(pitchLbl); + + // -- Spacer row (pushes waveform area down) -------------------------------- + lv_obj_t* spacer = mcContainer(root); + lv_obj_set_size(spacer, LV_PCT(100), rowH); + lv_obj_set_style_bg_opa(spacer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(spacer, 0, 0); + lv_obj_set_style_pad_all(spacer, 0, 0); + lv_obj_set_scrollable(spacer, false); + + // -- Waveform display (draw event) ----------------------------------------- + lv_obj_t* waveArea = mcContainer(root); + lv_obj_set_size(waveArea, LV_PCT(100), waveH); + lv_obj_set_style_bg_color(waveArea, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(waveArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(waveArea, 0, 0); + lv_obj_set_style_pad_all(waveArea, 0, 0); + lv_obj_set_scrollable(waveArea, false); + lv_obj_add_event_cb(waveArea, cbWaveDraw, LV_EVENT_DRAW_MAIN, mc); + lf.canvas = waveArea; + + // -- Value readout --------------------------------------------------------- + lv_obj_t* footer = mcContainer(root); + lv_obj_set_width(footer, LV_PCT(100)); + lv_obj_set_flex_grow(footer, 1); + lv_obj_set_style_bg_color(footer, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(footer, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(footer, 0, 0); + lv_obj_set_style_pad_all(footer, 16, 0); + lv_obj_set_scrollable(footer, false); + + lf.valueLbl = lv_label_create(footer); + lv_label_set_text(lf.valueLbl, "Val: 64"); + lv_obj_set_style_text_font(lf.valueLbl, fontNormal, 0); + lv_obj_set_style_text_color(lf.valueLbl, lv_color_hex(0x00DFFF), 0); + lv_obj_center(lf.valueLbl); +} diff --git a/Apps/MidiController/main/Source/ViewRaga.cpp b/Apps/MidiController/main/Source/ViewRaga.cpp new file mode 100644 index 0000000..bb936b8 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewRaga.cpp @@ -0,0 +1,554 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// RAGA view - Indian classical raga Markov-chain phrase generator +// +// Layout (flex column): +// [ctrl row: < | RAGA | status] +// [raga row: Raga- | name | Raga+] +// [tala row: Tala- | name | Tala+] +// [root row: Root- | C3 | Root+] +// [bpm row: BPM- | 120 | BPM+ ] +// [spacer flex_grow] +// [play row: PLAY/STOP | DRONE] +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Raga data +// --------------------------------------------------------------------------- + +static constexpr int RAGA_COUNT = 8; +static constexpr int TALA_COUNT = 3; + +static const char* RAGA_NAMES[RAGA_COUNT] = { + "Bhairavi", "Lalit", "Bhupali", "Todi", + "Madhuvanti", "Meghmalhar", "Yaman", "Malkauns" +}; +static const char* TALA_NAMES[TALA_COUNT] = { + "Teental", "Rupak", "Jhaptal" +}; + +static const uint8_t RAGA_INTERVALS[RAGA_COUNT][7] = { + {0, 1, 3, 5, 7, 8, 10}, // Bhairavi + {0, 1, 4, 5, 6, 9, 11}, // Lalit + {0, 2, 4, 7, 9, 0, 0}, // Bhupali (5 notes) + {0, 1, 3, 6, 7, 8, 11}, // Todi + {0, 2, 3, 6, 7, 9, 11}, // Madhuvanti + {0, 2, 5, 7, 10, 0, 0}, // Meghmalhar (5 notes) + {0, 2, 4, 6, 7, 9, 11}, // Yaman + {0, 3, 5, 8, 10, 0, 0}, // Malkauns (5 notes) +}; +static const uint8_t RAGA_LENGTHS[RAGA_COUNT] = {7, 7, 5, 7, 7, 5, 7, 5}; + +// Tala accent patterns (2=sam/strong, 1=khali/medium, 0=weak) +static const uint8_t TEENTAL_ACCENTS[16] = {2,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}; +static const uint8_t RUPAK_ACCENTS[7] = {2,0,0, 1,0,0,0}; +static const uint8_t JHAPTAL_ACCENTS[10] = {2,0,0, 1,0,0, 0,1,0,0}; + +static const int TALA_BEATS[TALA_COUNT] = {16, 7, 10}; +static const uint8_t* TALA_ACCENTS[TALA_COUNT] = { + TEENTAL_ACCENTS, RUPAK_ACCENTS, JHAPTAL_ACCENTS +}; + +// 2nd-order Markov weights for Bhairavi [prev_prev][prev][next] +static const uint8_t BHAIRAVI_WEIGHTS[7][7][7] = { + {{40,30,15,25,10,5,5},{20,20,40,35,10,5,5},{15,25,20,50,10,5,5}, + {30,20,30,30,15,10,5},{25,15,20,40,20,15,10},{20,15,10,25,15,20,30},{50,20,10,15,10,10,15}}, + {{40,30,20,20,10,5,5},{25,35,30,20,10,5,5},{15,20,25,45,15,5,5}, + {25,20,35,30,15,10,5},{20,25,20,35,20,10,5},{15,20,15,25,15,25,20},{45,25,15,15,10,10,10}}, + {{35,30,20,25,10,5,5},{35,25,25,25,10,5,5},{20,25,25,35,15,10,5}, + {20,15,20,40,20,20,10},{15,20,25,40,20,15,10},{10,15,15,30,15,25,25},{40,25,15,15,10,10,15}}, + {{45,30,20,20,10,5,5},{30,25,30,25,10,5,5},{25,35,25,25,10,5,5}, + {25,20,25,40,20,15,10},{20,15,20,40,25,15,10},{10,10,15,30,15,25,35},{50,20,15,20,10,10,10}}, + {{40,25,20,25,15,5,5},{25,25,25,30,15,10,5},{20,25,25,40,15,10,5}, + {20,20,25,40,20,15,10},{25,15,20,40,25,20,10},{15,15,15,30,15,30,25},{45,20,15,20,15,10,10}}, + {{40,25,20,20,15,10,10},{25,30,25,25,15,15,10},{20,25,25,35,15,15,10}, + {20,20,25,35,20,20,15},{20,20,20,35,25,20,15},{15,15,15,25,15,30,30},{45,20,15,20,15,15,15}}, + {{50,25,15,20,10,10,10},{30,30,25,25,15,10,5},{25,25,25,30,15,10,10}, + {25,20,25,35,15,15,10},{25,20,20,35,20,15,10},{20,20,15,25,15,25,25},{45,25,15,20,10,10,15}}, +}; + +// --------------------------------------------------------------------------- +// Algorithm helpers (file-static, operate on RagaState&) +// --------------------------------------------------------------------------- + +static int ragaSelectNextBhairavi(const int8_t hist[2]) { + int h0 = ((int)hist[0] % 7 + 7) % 7; + int h1 = ((int)hist[1] % 7 + 7) % 7; + const uint8_t* w = BHAIRAVI_WEIGHTS[h0][h1]; + int total = 0; + for (int i = 0; i < 7; i++) total += w[i]; + if (total <= 0) return 0; + int sel = (int)((uint32_t)esp_random() % (uint32_t)total); + int cum = 0; + for (int i = 0; i < 7; i++) { + cum += w[i]; + if (sel < cum) return i; + } + return 0; +} + +static void ragaGeneratePhrase(MidiController::RagaState& rs) { + int scaleLen = RAGA_LENGTHS[rs.raga]; + const uint8_t* scale = RAGA_INTERVALS[rs.raga]; + int total = 32; + rs.phraseLen = total; + + if (rs.raga == 0) { + // Bhairavi: 2nd-order Markov + int8_t hist[2] = {rs.history[0], rs.history[1]}; + int octave = 0; + for (int i = 0; i < total; i++) { + int degree = ragaSelectNextBhairavi(hist); + if ((uint32_t)(esp_random() % 100) < 8) { + int shift = (int)((uint32_t)(esp_random() % 3)) - 1; + octave = std::max(-1, std::min(1, octave + shift)); + } + rs.phrase[i] = (int8_t)(scale[degree] + octave * 12); + hist[0] = hist[1]; + hist[1] = (int8_t)degree; + } + rs.history[0] = hist[0]; + rs.history[1] = hist[1]; + } else { + // Other ragas: random walk + int degree = (int)((uint32_t)(esp_random() % (uint32_t)scaleLen)); + int octave = 0; + for (int i = 0; i < total; i++) { + int step = (int)((uint32_t)(esp_random() % 3)) - 1; + degree = std::max(0, std::min(scaleLen - 1, degree + step)); + if ((uint32_t)(esp_random() % 100) < 10) { + int shift = (int)((uint32_t)(esp_random() % 3)) - 1; + octave = std::max(-1, std::min(1, octave + shift)); + } + rs.phrase[i] = (int8_t)(scale[degree] + octave * 12); + } + } + rs.phraseIdx = 0; +} + +static void ragaUpdateTempo(MidiController::RagaState& rs) { + if (rs.bpm < 1) rs.bpm = 120; // Safe default + uint32_t interval = 60000u / (uint32_t)rs.bpm; + rs.noteInterval = std::max((uint32_t)40, interval / 4); + rs.noteDuration = std::max((uint32_t)30, rs.noteInterval * 8 / 10); +} + +static void ragaStopNote(MidiController* mc) { + auto& rs = mc->raga_; + if (rs.noteActive) { + mc->sendMidi(0x80, rs.currentNote, 0); + rs.noteActive = false; + } +} + +static void ragaUpdateDrone(MidiController* mc) { + auto& rs = mc->raga_; + if (rs.droneOn) { + if (!rs.droneActive || rs.droneNote != (uint8_t)rs.rootNote) { + if (rs.droneActive) mc->sendMidi(0x80, rs.droneNote, 0); + rs.droneNote = (uint8_t)rs.rootNote; + rs.droneActive = true; + mc->sendMidi(0x90, rs.droneNote, 80); + } + } else if (rs.droneActive) { + mc->sendMidi(0x80, rs.droneNote, 0); + rs.droneActive = false; + } +} + +// --------------------------------------------------------------------------- +// Timer callback +// --------------------------------------------------------------------------- + +static void cbRagaTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + auto& rs = mc->raga_; + + ragaUpdateDrone(mc); + if (!rs.playing) return; + + uint32_t now = lv_tick_get(); + + // Note-off check + if (rs.noteActive && now >= rs.noteOffMs) { + ragaStopNote(mc); + } + + // Schedule next note + if (now >= rs.nextNoteMs) { + if (rs.phraseLen == 0 || rs.phraseIdx >= rs.phraseLen) + ragaGeneratePhrase(rs); + + ragaStopNote(mc); + uint8_t note = (uint8_t)std::max(0, std::min(127, rs.rootNote + rs.phrase[rs.phraseIdx++])); + // Ensure talaBeat is within bounds for current tala + if (rs.talaBeat >= TALA_BEATS[rs.tala]) rs.talaBeat = 0; + uint8_t accent = TALA_ACCENTS[rs.tala][rs.talaBeat]; + uint8_t vel = (uint8_t)std::min(127, 90 + (int)accent * 10); + mc->sendMidi(0x90, note, vel); + rs.currentNote = note; + rs.noteActive = true; + rs.noteOffMs = now + rs.noteDuration; + + // Update status label + if (rs.statusLbl) { + char buf[32]; + char noteName[8]; midiNoteName(note, noteName, sizeof(noteName)); + snprintf(buf, sizeof(buf), "%s | beat %d/%d", + noteName, rs.talaBeat + 1, TALA_BEATS[rs.tala]); + lv_label_set_text(rs.statusLbl, buf); + } + rs.talaBeat = (rs.talaBeat + 1) % TALA_BEATS[rs.tala]; + rs.nextNoteMs = now + rs.noteInterval; + } +} + +// Forward declarations for public encoder entry points (defined at file end) +void ragaTogglePlay (MidiController* mc); +void ragaToggleDrone(MidiController* mc); + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbPlayStop(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) ragaTogglePlay(mc); +} + +static void cbDrone(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) ragaToggleDrone(mc); +} + +static void cbRagaDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rs = mc->raga_; + rs.raga = (rs.raga + RAGA_COUNT - 1) % RAGA_COUNT; + ragaGeneratePhrase(rs); + if (rs.ragaLbl) lv_label_set_text(rs.ragaLbl, RAGA_NAMES[rs.raga]); + if (rs.ragaBigLbl) lv_label_set_text(rs.ragaBigLbl, RAGA_NAMES[rs.raga]); +} +static void cbRagaUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rs = mc->raga_; + rs.raga = (rs.raga + 1) % RAGA_COUNT; + ragaGeneratePhrase(rs); + if (rs.ragaLbl) lv_label_set_text(rs.ragaLbl, RAGA_NAMES[rs.raga]); + if (rs.ragaBigLbl) lv_label_set_text(rs.ragaBigLbl, RAGA_NAMES[rs.raga]); +} + +static void cbTalaDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rs = mc->raga_; + rs.tala = (rs.tala + TALA_COUNT - 1) % TALA_COUNT; + rs.talaBeat = 0; + if (rs.talaLbl) lv_label_set_text(rs.talaLbl, TALA_NAMES[rs.tala]); + if (rs.talaBigLbl) lv_label_set_text(rs.talaBigLbl, TALA_NAMES[rs.tala]); +} +static void cbTalaUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rs = mc->raga_; + rs.tala = (rs.tala + 1) % TALA_COUNT; + rs.talaBeat = 0; + if (rs.talaLbl) lv_label_set_text(rs.talaLbl, TALA_NAMES[rs.tala]); + if (rs.talaBigLbl) lv_label_set_text(rs.talaBigLbl, TALA_NAMES[rs.tala]); +} + +static void cbRootDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rs = mc->raga_; + if (rs.rootNote > 24) rs.rootNote--; + if (rs.rootLbl) { + char buf[8]; midiNoteName(rs.rootNote, buf, sizeof(buf)); + lv_label_set_text(rs.rootLbl, buf); + } +} +static void cbRootUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rs = mc->raga_; + if (rs.rootNote < 96) rs.rootNote++; + if (rs.rootLbl) { + char buf[8]; midiNoteName(rs.rootNote, buf, sizeof(buf)); + lv_label_set_text(rs.rootLbl, buf); + } +} + +static void cbBpmDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rs = mc->raga_; + if (rs.bpm > 40) rs.bpm -= 5; + ragaUpdateTempo(rs); + if (rs.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", rs.bpm); + lv_label_set_text(rs.bpmLbl, buf); + } +} +static void cbBpmUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rs = mc->raga_; + if (rs.bpm < 240) rs.bpm += 5; + ragaUpdateTempo(rs); + if (rs.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", rs.bpm); + lv_label_set_text(rs.bpmLbl, buf); + } +} + +// --------------------------------------------------------------------------- +// Build +// --------------------------------------------------------------------------- + +void buildRagaView(MidiController* mc, lv_obj_t* parent) { + auto& rs = mc->raga_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + const lv_font_t* fontLarge = lvgl_get_text_font(FONT_SIZE_LARGE); + + static constexpr int rowH = 88; + static constexpr int btnH = 64; + static constexpr int pad = 12; + + ragaUpdateTempo(rs); + + // -- Root: flex column ----------------------------------------------------- + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // -- Top control row ------------------------------------------------------ + { + lv_obj_t* ctrl = mcRow(root, rowH); + + // Back + mcBackBtn(ctrl, cbBack, mc, MC_W_S); + + // Title + lv_obj_t* title = lv_label_create(ctrl); + lv_label_set_text(title, "RAGA"); + lv_obj_set_style_text_font(title, fontNormal, 0); + lv_obj_set_style_text_color(title, lv_color_hex(0xFF8844), 0); + lv_obj_set_flex_grow(title, 1); + + // Play / Stop button in the top bar - sync to current state + rs.playBtn = mcButton(ctrl); + lv_obj_set_size(rs.playBtn, 160, btnH); + lv_obj_set_style_bg_color(rs.playBtn, + rs.playing ? lv_color_hex(0xFF2020) : lv_color_hex(0x00AA44), 0); + lv_obj_add_event_cb(rs.playBtn, cbPlayStop, LV_EVENT_CLICKED, mc); + rs.playLbl = lv_label_create(rs.playBtn); + lv_label_set_text(rs.playLbl, rs.playing ? "STOP" : "PLAY"); + lv_obj_set_style_text_font(rs.playLbl, fontSmall, 0); + lv_obj_center(rs.playLbl); + + // Drone button - sync to current state + rs.droneBtn = mcButton(ctrl); + lv_obj_set_size(rs.droneBtn, 200, btnH); + lv_obj_set_style_bg_color(rs.droneBtn, + rs.droneOn ? lv_color_hex(0x00AA44) : lv_color_hex(0x1A2A40), LV_PART_MAIN); + lv_obj_add_event_cb(rs.droneBtn, cbDrone, LV_EVENT_CLICKED, mc); + rs.droneLbl = lv_label_create(rs.droneBtn); + lv_label_set_text(rs.droneLbl, rs.droneOn ? "DRONE ON" : "DRONE OFF"); + lv_obj_set_style_text_font(rs.droneLbl, fontSmall, 0); + lv_obj_center(rs.droneLbl); + + // Status + rs.statusLbl = lv_label_create(ctrl); + lv_label_set_text(rs.statusLbl, "IDLE"); + lv_obj_set_style_text_font(rs.statusLbl, fontSmall, 0); + lv_obj_set_style_text_color(rs.statusLbl, lv_color_hex(0x666688), 0); + lv_obj_set_width(rs.statusLbl, 260); + lv_obj_set_style_text_align(rs.statusLbl, LV_TEXT_ALIGN_RIGHT, 0); + } + + // -- Body: flex row - left params | right display -------------------------- + lv_obj_t* body = mcContainer(root); + lv_obj_set_width(body, LV_PCT(100)); + lv_obj_set_flex_grow(body, 1); + lv_obj_set_flex_flow(body, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(body, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_bg_opa(body, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(body, 0, 0); + lv_obj_set_style_pad_all(body, 0, 0); + lv_obj_set_style_pad_gap(body, 0, 0); + lv_obj_set_scrollable(body, false); + + // -- LEFT: parameter column (flex column, 600px) --------------------------- + { + lv_obj_t* col = mcContainer(body); + lv_obj_set_size(col, 600, LV_PCT(100)); + lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(col, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_bg_opa(col, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(col, 0, 0); + lv_obj_set_style_pad_all(col, 0, 0); + lv_obj_set_style_pad_gap(col, 0, 0); + lv_obj_set_scrollable(col, false); + + // Each param gets flex_grow=1 so all 4 fill the column equally + auto makeParamRow = [&](const char* name, lv_color_t nameCol) -> lv_obj_t* { + lv_obj_t* row = mcContainer(col); + lv_obj_set_width(row, LV_PCT(100)); + lv_obj_set_flex_grow(row, 1); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(row, lv_color_hex(0x0A0C14), 0); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_style_border_color(row, lv_color_hex(0x1A2030), 0); + lv_obj_set_style_pad_all(row, pad, 0); + lv_obj_set_style_pad_gap(row, pad, 0); + lv_obj_set_scrollable(row, false); + lv_obj_t* hdr = lv_label_create(row); + lv_label_set_text(hdr, name); + lv_obj_set_style_text_font(hdr, fontSmall, 0); + lv_obj_set_style_text_color(hdr, nameCol, 0); + lv_obj_set_width(hdr, 120); + return row; + }; + + auto makeBtn = [&](lv_obj_t* par, const char* txt, int w, lv_event_cb_t cb) -> lv_obj_t* { + return mcSmallBtn(par, txt, w, cb, mc); + }; + + // Value label uses flex_grow=1 so it takes remaining width - prevents > button overflow + auto makeValLbl = [&](lv_obj_t* par, const char* txt, lv_color_t col) -> lv_obj_t* { + lv_obj_t* lbl = lv_label_create(par); + lv_label_set_text(lbl, txt); + lv_obj_set_style_text_font(lbl, fontNormal, 0); + lv_obj_set_style_text_color(lbl, col, 0); + lv_obj_set_flex_grow(lbl, 1); + lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0); + return lbl; + }; + + // Raga + { + lv_obj_t* row = makeParamRow("Raga", lv_color_hex(0x888888)); + makeBtn(row, "<", 88, cbRagaDown); + rs.ragaLbl = makeValLbl(row, RAGA_NAMES[rs.raga], lv_color_hex(0xFF8844)); + makeBtn(row, ">", 88, cbRagaUp); + } + // Tala + { + lv_obj_t* row = makeParamRow("Tala", lv_color_hex(0x888888)); + makeBtn(row, "<", 88, cbTalaDown); + rs.talaLbl = makeValLbl(row, TALA_NAMES[rs.tala], lv_color_hex(0xAADDFF)); + makeBtn(row, ">", 88, cbTalaUp); + } + // Root + { + lv_obj_t* row = makeParamRow("Root", lv_color_hex(0x888888)); + makeBtn(row, "<", 88, cbRootDown); + char rootBuf[8]; midiNoteName(rs.rootNote, rootBuf, sizeof(rootBuf)); + rs.rootLbl = makeValLbl(row, rootBuf, MC_VALUE()); + makeBtn(row, ">", 88, cbRootUp); + } + // BPM + { + lv_obj_t* row = makeParamRow("BPM", lv_color_hex(0x888888)); + makeBtn(row, "<", 88, cbBpmDown); + char bpmBuf[8]; snprintf(bpmBuf, sizeof(bpmBuf), "%d", rs.bpm); + rs.bpmLbl = makeValLbl(row, bpmBuf, lv_color_hex(0xFFE000)); + makeBtn(row, ">", 88, cbBpmUp); + } + } + + // -- RIGHT: status display (flex_grow) ------------------------------------- + { + lv_obj_t* display = mcContainer(body); + lv_obj_set_height(display, LV_PCT(100)); + lv_obj_set_flex_grow(display, 1); + lv_obj_set_flex_flow(display, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(display, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_color(display, lv_color_hex(0x05080F), 0); + lv_obj_set_style_bg_opa(display, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(display, 0, 0); + lv_obj_set_style_pad_all(display, pad * 2, 0); + lv_obj_set_style_pad_gap(display, pad, 0); + lv_obj_set_scrollable(display, false); + + // Large raga name (updates when raga changes) + rs.ragaBigLbl = lv_label_create(display); + lv_label_set_text(rs.ragaBigLbl, RAGA_NAMES[rs.raga]); + lv_obj_set_style_text_font(rs.ragaBigLbl, fontLarge, 0); + lv_obj_set_style_text_color(rs.ragaBigLbl, lv_color_hex(0xFF8844), 0); + lv_obj_set_style_text_align(rs.ragaBigLbl, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_width(rs.ragaBigLbl, LV_PCT(100)); + + // Tala info (updates when tala changes) + rs.talaBigLbl = lv_label_create(display); + lv_label_set_text(rs.talaBigLbl, TALA_NAMES[rs.tala]); + lv_obj_set_style_text_font(rs.talaBigLbl, fontNormal, 0); + lv_obj_set_style_text_color(rs.talaBigLbl, lv_color_hex(0xAADDFF), 0); + lv_obj_set_style_text_align(rs.talaBigLbl, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_width(rs.talaBigLbl, LV_PCT(100)); + } + + // -- Start timer ---------------------------------------------------------- + mc->ragaTimer_ = lv_timer_create(cbRagaTimer, 20, mc); +} + +// --------------------------------------------------------------------------- +// Public encoder entry points (called from EncoderInput.cpp) +// --------------------------------------------------------------------------- + +void ragaTogglePlay(MidiController* mc) { + auto& rs = mc->raga_; + rs.playing = !rs.playing; + if (rs.playing) { + ragaUpdateTempo(rs); + ragaGeneratePhrase(rs); + rs.talaBeat = 0; + rs.nextNoteMs = lv_tick_get(); + } else { + ragaStopNote(mc); + if (rs.statusLbl) lv_label_set_text(rs.statusLbl, "IDLE"); + } + if (rs.playLbl) lv_label_set_text(rs.playLbl, rs.playing ? "STOP" : "PLAY"); + if (rs.playBtn) + lv_obj_set_style_bg_color(rs.playBtn, + rs.playing ? lv_color_hex(0xFF2020) : lv_color_hex(0x00AA44), 0); +} + +void ragaToggleDrone(MidiController* mc) { + auto& rs = mc->raga_; + rs.droneOn = !rs.droneOn; + if (rs.droneLbl) lv_label_set_text(rs.droneLbl, rs.droneOn ? "DRONE ON" : "DRONE OFF"); + if (rs.droneBtn) { + lv_obj_set_style_bg_color(rs.droneBtn, + rs.droneOn ? lv_color_hex(0x00AA44) : lv_color_hex(0x1A2A40), + LV_PART_MAIN); + } +} diff --git a/Apps/MidiController/main/Source/ViewRng.cpp b/Apps/MidiController/main/Source/ViewRng.cpp new file mode 100644 index 0000000..8f5d95a --- /dev/null +++ b/Apps/MidiController/main/Source/ViewRng.cpp @@ -0,0 +1,352 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// RNG view - Random melody generator +// +// All controls, no canvas. +// Layout (flex column): +// [header row] +// [play controls: PLAY/STOP | Key -/+/label | Scale] +// [octave range: MinOct -/label/+ | MaxOct -/label/+] +// [probability: Prob -/bar/+] +// [BPM + subdivision] +// [current note display - flex_grow] +// --------------------------------------------------------------------------- + +static const char* SUBDIV_NAMES[] = { "1/4", "1/8", "1/16" }; +static const int SUBDIV_DIVS[] = { 1, 2, 4 }; + + +static uint64_t rngNoteIntervalUs(MidiController* mc) { + auto& rn = mc->rng_; + int idx = rn.subdivision % (sizeof(SUBDIV_DIVS) / sizeof(SUBDIV_DIVS[0])); + float quarterUs = 60000000.0f / (float)rn.bpm; + return (uint64_t)(quarterUs / (float)SUBDIV_DIVS[idx]); +} + +static void cbRngTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc || !mc->rng_.playing) return; + auto& rn = mc->rng_; + + uint64_t nowUs = (uint64_t)esp_timer_get_time(); + + // Handle note-off + if (rn.noteOffUs > 0 && nowUs >= rn.noteOffUs) { + if (rn.currentNote >= 0) { + mc->sendMidi(0x80, (uint8_t)rn.currentNote, 0); + rn.currentNote = -1; + } + rn.noteOffUs = 0; + } + + // Trigger next note + if (nowUs >= rn.nextNoteUs) { + uint64_t interval = rngNoteIntervalUs(mc); + rn.nextNoteUs = nowUs + interval; + + if ((int)(esp_random() % 100) < rn.probability) { + // Pick random degree within scale + int numDegrees = MIDI_SCALES[rn.scaleIdx % NUM_MIDI_SCALES].numNotes; + int degree = (int)(esp_random() % (uint32_t)numDegrees); + int octRange = rn.maxOctave - rn.minOctave + 1; + if (octRange < 1) octRange = 1; + int octave = rn.minOctave + (int)(esp_random() % (uint32_t)octRange); + int note = getNoteInScale(rn.scaleIdx, degree, octave, rn.rootKey); + + // Note off for previous if still playing + if (rn.currentNote >= 0) { + mc->sendMidi(0x80, (uint8_t)rn.currentNote, 0); + } + + mc->sendMidi(0x90, (uint8_t)note, 80 + (int)(esp_random() % 40)); + rn.currentNote = note; + rn.noteOffUs = nowUs + interval * 3 / 4; // note length = 75% of interval + + if (rn.noteLbl) { + char buf[16]; + char noteName[8]; + midiNoteName(note, noteName, sizeof(noteName)); + snprintf(buf, sizeof(buf), "%s", noteName); + lv_label_set_text(rn.noteLbl, buf); + } + } else { + // Rest - no note + if (rn.noteLbl) lv_label_set_text(rn.noteLbl, "( rest )"); + } + } +} + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbPlayStop(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& rn = mc->rng_; + rn.playing = !rn.playing; + if (rn.playing) { + rn.nextNoteUs = (uint64_t)esp_timer_get_time(); + rn.noteOffUs = 0; + rn.currentNote = -1; + if (!mc->rngTimer_) { + mc->rngTimer_ = lv_timer_create(cbRngTimer, 10, mc); + } + } else { + if (rn.currentNote >= 0) { + mc->sendMidi(0x80, (uint8_t)rn.currentNote, 0); + rn.currentNote = -1; + } + if (mc->rngTimer_) { lv_timer_delete(mc->rngTimer_); mc->rngTimer_ = nullptr; } + if (rn.noteLbl) lv_label_set_text(rn.noteLbl, ""); + } + if (rn.playLbl) lv_label_set_text(rn.playLbl, rn.playing ? "STOP" : "PLAY"); + if (rn.playBtn) { + lv_obj_set_style_bg_color(rn.playBtn, + rn.playing ? lv_color_hex(0xFF2020) : lv_color_hex(0x00AA44), 0); + } +} + +static void cbKeyDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->rng_.rootKey = (mc->rng_.rootKey + 11) % 12; + if (mc->rng_.keyLbl) lv_label_set_text(mc->rng_.keyLbl, KEY_NAMES[mc->rng_.rootKey]); +} +static void cbKeyUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->rng_.rootKey = (mc->rng_.rootKey + 1) % 12; + if (mc->rng_.keyLbl) lv_label_set_text(mc->rng_.keyLbl, KEY_NAMES[mc->rng_.rootKey]); +} + +static void cbScaleCycle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->rng_.scaleIdx = (mc->rng_.scaleIdx + 1) % NUM_MIDI_SCALES; + if (mc->rng_.scaleLbl) lv_label_set_text(mc->rng_.scaleLbl, MIDI_SCALES[mc->rng_.scaleIdx].name); +} + +static void cbMinOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->rng_.minOctave > 1) mc->rng_.minOctave--; + if (mc->rng_.minOctLbl) { + char buf[4]; snprintf(buf, sizeof(buf), "%d", mc->rng_.minOctave); + lv_label_set_text(mc->rng_.minOctLbl, buf); + } +} +static void cbMinOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->rng_.minOctave < mc->rng_.maxOctave) mc->rng_.minOctave++; + if (mc->rng_.minOctLbl) { + char buf[4]; snprintf(buf, sizeof(buf), "%d", mc->rng_.minOctave); + lv_label_set_text(mc->rng_.minOctLbl, buf); + } +} +static void cbMaxOctDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->rng_.maxOctave > mc->rng_.minOctave) mc->rng_.maxOctave--; + if (mc->rng_.maxOctLbl) { + char buf[4]; snprintf(buf, sizeof(buf), "%d", mc->rng_.maxOctave); + lv_label_set_text(mc->rng_.maxOctLbl, buf); + } +} +static void cbMaxOctUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->rng_.maxOctave < 8) mc->rng_.maxOctave++; + if (mc->rng_.maxOctLbl) { + char buf[4]; snprintf(buf, sizeof(buf), "%d", mc->rng_.maxOctave); + lv_label_set_text(mc->rng_.maxOctLbl, buf); + } +} + +static void cbProbDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->rng_.probability -= 5; if (mc->rng_.probability < 0) mc->rng_.probability = 0; + if (mc->rng_.probLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d%%", mc->rng_.probability); + lv_label_set_text(mc->rng_.probLbl, buf); + } +} +static void cbProbUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->rng_.probability += 5; if (mc->rng_.probability > 100) mc->rng_.probability = 100; + if (mc->rng_.probLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d%%", mc->rng_.probability); + lv_label_set_text(mc->rng_.probLbl, buf); + } +} + +static void cbBpmDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->rng_.bpm -= 5; if (mc->rng_.bpm < 40) mc->rng_.bpm = 40; + if (mc->rng_.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", mc->rng_.bpm); + lv_label_set_text(mc->rng_.bpmLbl, buf); + } +} +static void cbBpmUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->rng_.bpm += 5; if (mc->rng_.bpm > 300) mc->rng_.bpm = 300; + if (mc->rng_.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", mc->rng_.bpm); + lv_label_set_text(mc->rng_.bpmLbl, buf); + } +} + +static void cbSubdivDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->rng_.subdivision > 0) mc->rng_.subdivision--; + if (mc->rng_.subdivLbl) lv_label_set_text(mc->rng_.subdivLbl, SUBDIV_NAMES[mc->rng_.subdivision]); +} +static void cbSubdivUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + if (mc->rng_.subdivision < 2) mc->rng_.subdivision++; + if (mc->rng_.subdivLbl) lv_label_set_text(mc->rng_.subdivLbl, SUBDIV_NAMES[mc->rng_.subdivision]); +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildRngView(MidiController* mc, lv_obj_t* parent) { + auto& rn = mc->rng_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + const lv_font_t* fontLarge = lvgl_get_text_font(FONT_SIZE_LARGE); + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 1, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // Helper to make a row + auto makeRow = [&]() -> lv_obj_t* { return mcRow(root); }; + + auto makeBtn = [&](lv_obj_t* parent_, const char* txt, int w, lv_event_cb_t cb) { + mcSmallBtn(parent_, txt, w, cb, mc); + }; + + auto makeLbl = [&](lv_obj_t* parent_, const char* txt, int w, lv_color_t col) -> lv_obj_t* { + return mcValue(parent_, txt, w, col); + }; + + auto makeHdr = [&](lv_obj_t* parent_, const char* txt) { + mcHead(parent_, txt); + }; + + // -- Row 1: header + play ------------------------------------------------- + lv_obj_t* row1 = makeRow(); + mcBackBtn(row1, cbBack, mc, MC_W_M); + lv_obj_t* titleLbl = lv_label_create(row1); + lv_label_set_text(titleLbl, "RNG JAMS"); + lv_obj_set_style_text_font(titleLbl, fontNormal, 0); + lv_obj_set_style_text_color(titleLbl, lv_color_hex(0x44FF88), 0); + lv_obj_set_flex_grow(titleLbl, 1); + + rn.playBtn = mcButton(row1); + lv_obj_set_size(rn.playBtn, 224, MC_BTN_H); + lv_obj_set_style_bg_color(rn.playBtn, lv_color_hex(0x00AA44), 0); + lv_obj_add_event_cb(rn.playBtn, cbPlayStop, LV_EVENT_CLICKED, mc); + rn.playLbl = lv_label_create(rn.playBtn); + lv_label_set_text(rn.playLbl, "PLAY"); + lv_obj_set_style_text_font(rn.playLbl, fontSmall, 0); + lv_obj_center(rn.playLbl); + + // -- Row 2: Key + Scale --------------------------------------------------- + lv_obj_t* row2 = makeRow(); + makeHdr(row2, "Key:"); + makeBtn(row2, "-", 96, cbKeyDown); + rn.keyLbl = makeLbl(row2, KEY_NAMES[rn.rootKey], 112, MC_VALUE()); + makeBtn(row2, "+", 96, cbKeyUp); + + makeHdr(row2, " Scale:"); + lv_obj_t* scaleBtn = mcButton(row2); + lv_obj_set_size(scaleBtn, 240, MC_BTN_H); + lv_obj_add_event_cb(scaleBtn, cbScaleCycle, LV_EVENT_CLICKED, mc); + rn.scaleLbl = lv_label_create(scaleBtn); + lv_label_set_text(rn.scaleLbl, MIDI_SCALES[rn.scaleIdx % NUM_MIDI_SCALES].name); + lv_obj_set_style_text_font(rn.scaleLbl, fontSmall, 0); + lv_obj_center(rn.scaleLbl); + + // -- Row 3: Oct range ----------------------------------------------------- + lv_obj_t* row3 = makeRow(); + makeHdr(row3, "Min:"); + makeBtn(row3, "-", 96, cbMinOctDown); + char mob[4]; snprintf(mob, sizeof(mob), "%d", rn.minOctave); + rn.minOctLbl = makeLbl(row3, mob, 96, lv_color_hex(0xFFFFFF)); + makeBtn(row3, "+", 96, cbMinOctUp); + + makeHdr(row3, " Max:"); + makeBtn(row3, "-", 96, cbMaxOctDown); + char maxb[4]; snprintf(maxb, sizeof(maxb), "%d", rn.maxOctave); + rn.maxOctLbl = makeLbl(row3, maxb, 96, lv_color_hex(0xFFFFFF)); + makeBtn(row3, "+", 96, cbMaxOctUp); + + // -- Row 4: Probability --------------------------------------------------- + lv_obj_t* row4 = makeRow(); + makeHdr(row4, "Prob:"); + makeBtn(row4, "-", 96, cbProbDown); + char pb[8]; snprintf(pb, sizeof(pb), "%d%%", rn.probability); + rn.probLbl = makeLbl(row4, pb, 176, lv_color_hex(0xFFE000)); + makeBtn(row4, "+", 96, cbProbUp); + + // -- Row 5: BPM + Subdivision --------------------------------------------- + lv_obj_t* row5 = makeRow(); + makeHdr(row5, "BPM:"); + makeBtn(row5, "-", 96, cbBpmDown); + char bpmb[8]; snprintf(bpmb, sizeof(bpmb), "%d", rn.bpm); + rn.bpmLbl = makeLbl(row5, bpmb, 176, lv_color_hex(0x00DFFF)); + makeBtn(row5, "+", 96, cbBpmUp); + + makeHdr(row5, " Beat:"); + makeBtn(row5, "<", 96, cbSubdivDown); + rn.subdivLbl = makeLbl(row5, SUBDIV_NAMES[rn.subdivision], 144, lv_color_hex(0x00DFFF)); + makeBtn(row5, ">", 96, cbSubdivUp); + + // -- Note display (fills remaining space) -------------------------------- + lv_obj_t* noteArea = mcContainer(root); + lv_obj_set_width(noteArea, LV_PCT(100)); + lv_obj_set_flex_grow(noteArea, 1); + lv_obj_set_style_bg_color(noteArea, lv_color_hex(0x050508), 0); + lv_obj_set_style_bg_opa(noteArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(noteArea, 0, 0); + lv_obj_set_style_pad_all(noteArea, 0, 0); + lv_obj_set_scrollable(noteArea, false); + + rn.noteLbl = lv_label_create(noteArea); + lv_label_set_text(rn.noteLbl, ""); + lv_obj_set_style_text_font(rn.noteLbl, fontLarge, 0); + lv_obj_set_style_text_color(rn.noteLbl, lv_color_hex(0x44FF88), 0); + lv_obj_center(rn.noteLbl); +} diff --git a/Apps/MidiController/main/Source/ViewTb3po.cpp b/Apps/MidiController/main/Source/ViewTb3po.cpp new file mode 100644 index 0000000..bd14f1d --- /dev/null +++ b/Apps/MidiController/main/Source/ViewTb3po.cpp @@ -0,0 +1,636 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// TB3PO view - 303-style acid bassline generator +// +// Layout (flex column): +// [ctrl row: < | PLAY | BPM- | N | BPM+ DENS- | D:N | DENS+ REGEN] +// [canvas flex_grow: 4-row step visualization] +// [status row: KEY- | key | KEY+ SCALE- | scale | SCALE+ Seed: XXXXXXXX] +// --------------------------------------------------------------------------- + + +// --------------------------------------------------------------------------- +// Scale data +// --------------------------------------------------------------------------- + +static constexpr int TB3_NUM_SCALES = 6; +static const char* TB3_SCALE_NAMES[TB3_NUM_SCALES] = { + "Major", "Minor", "Dorian", "Penta", "Blues", "Chrome" +}; +static constexpr int TB3_MAX_SCALE_LEN = 12; +static const int TB3_SCALE_INTERVALS[TB3_NUM_SCALES][TB3_MAX_SCALE_LEN] = { + {0, 2, 4, 5, 7, 9, 11}, // Major (7) + {0, 2, 3, 5, 7, 8, 10}, // Minor (7) + {0, 2, 3, 5, 7, 9, 10}, // Dorian (7) + {0, 2, 4, 7, 9}, // Penta (5) + {0, 3, 5, 6, 7, 10}, // Blues (6) + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, // Chrome (12) +}; +static const int TB3_SCALE_LENS[TB3_NUM_SCALES] = {7, 7, 7, 5, 6, 12}; + +// KEY_NAMES already defined in MidiScales.h - use it directly + +// Get MIDI note for scale degree + octave (root at MIDI 48 = C3) +static int tb3NoteInScale(int scaleIdx, int degree, int octave, int key) { + if (scaleIdx < 0 || scaleIdx >= TB3_NUM_SCALES) scaleIdx = 0; + int len = TB3_SCALE_LENS[scaleIdx]; + // Proper modular arithmetic for negative degrees + while (degree < 0) degree += len; + int deg = degree % len; + int octShift = degree / len; + int rootMidi = 48 + key; // C3 + key offset + int note = rootMidi + TB3_SCALE_INTERVALS[scaleIdx][deg] + (octave - 3 + octShift) * 12; + if (note < 0) note = 0; + if (note > 127) note = 127; + return note; +} + +// --------------------------------------------------------------------------- +// Pattern generation +// --------------------------------------------------------------------------- + +static void tb3Regenerate(MidiController::Tb3poState& ts) { + // Use seed-based PRNG + uint32_t rng = ts.seed; + auto rnd = [&](int lo, int hi) -> int { + rng = rng * 1664525u + 1013904223u; + if (hi <= lo) return lo; + return lo + (int)((rng >> 16) % (uint32_t)(hi - lo)); + }; + auto rndBit = [&](int prob) -> bool { + return rnd(0, 100) < prob; + }; + + int scaleIdx = ts.scale; + if (scaleIdx < 0 || scaleIdx >= TB3_NUM_SCALES) scaleIdx = 0; + int scaleLen = TB3_SCALE_LENS[scaleIdx]; + int pitchDens = std::min(ts.density, 8); + int available; + if (pitchDens > 7) available = scaleLen - 1; + else if (pitchDens < 2) available = pitchDens; + else { + int fromScale = std::max(4, scaleLen - 3); + available = 3 + (pitchDens - 3) * fromScale / 4; + available = std::max(1, std::min(scaleLen - 1, available)); + } + + // Generate pitches, oct flags - use direct bit position (1u << i) to avoid inversion + uint32_t oct_ups = 0, oct_downs = 0; + int noteDegs[32] = {}; + for (int i = 0; i < 32; i++) { + int forceRepeatProb = 50 - (pitchDens * 6); + if (i > 0 && rndBit(forceRepeatProb)) { + noteDegs[i] = noteDegs[i-1]; + } else { + noteDegs[i] = rnd(0, available + 1); + if (rndBit(40)) { + if (rndBit(50)) oct_ups |= (1u << i); + else oct_downs |= (1u << i); + } + } + } + + // Generate gates, slides, accents - direct bit position, no shift accumulation + rng = ts.seed + 1000u; + // Gate probability rises monotonically with density (10% at 0, 90% at 16). + int densProb = 10 + ts.density * 5; + uint32_t gates = 0, slides = 0, accents = 0; + int latestSlide = 0, latestAccent = 0; + for (int i = 0; i < 32; i++) { + if (rndBit(densProb)) gates |= (1u << i); + latestSlide = rndBit(latestSlide ? 10 : 18); + if (latestSlide) slides |= (1u << i); + latestAccent = rndBit(latestAccent ? 7 : 16); + if (latestAccent) accents |= (1u << i); + } + + // Fill steps array + for (int i = 0; i < 32; i++) { + auto& s = ts.steps[i]; + int scaleDeg = noteDegs[i]; + bool octUp = (oct_ups & (1u << i)) != 0; + bool octDown = (oct_downs & (1u << i)) != 0; + s.note = scaleDeg; // scale degree (resolved to MIDI in timer) + s.oct_up = octUp; + s.oct_down = octDown; + s.accent = (accents & (1u << i)) != 0; + s.slide = (slides & (1u << i)) != 0; + s.gate = (gates & (1u << i)) != 0; + } +} + +static int tb3StepToMidi(const MidiController::Tb3poState& ts, int stepIdx) { + const auto& s = ts.steps[stepIdx]; + int octOff = ts.octave + (s.oct_up ? 1 : (s.oct_down ? -1 : 0)); + return tb3NoteInScale(ts.scale, s.note, octOff, ts.key); +} + +// --------------------------------------------------------------------------- +// Canvas draw +// --------------------------------------------------------------------------- + +// Draw the grid directly. The old implementation recreated 128 cell widgets +// (plus labels and dots) on every step, which is far too heavy for the LVGL +// task and can starve the idle watchdog on slower targets. +static void cbTb3CanvasDraw(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + + lv_obj_t* obj = lv_event_get_target_obj(e); + lv_layer_t* layer = lv_event_get_layer(e); + + lv_area_t area; + lv_obj_get_coords(obj, &area); + int32_t ox = area.x1; + int32_t oy = area.y1; + int W = lv_area_get_width(&area); + int H = lv_area_get_height(&area); + if (W <= 32 || H <= 32) return; + + ts.canvasW = W; + ts.canvasH = H; + + // Background fill + lv_draw_fill_dsc_t bg; + lv_draw_fill_dsc_init(&bg); + bg.color = lv_color_hex(0x050810); + bg.opa = LV_OPA_COVER; + lv_draw_fill(layer, &bg, &area); + + int stepW = W / 32; + int rowH = H / 4; + int cellW = stepW - 1; + int cellH = rowH - 1; + + const lv_font_t* font = lvgl_get_text_font(FONT_SIZE_SMALL); + int lineH = lv_font_get_line_height(font); + + lv_draw_rect_dsc_t cell; + lv_draw_rect_dsc_init(&cell); + cell.radius = 0; + cell.bg_opa = LV_OPA_COVER; + cell.border_width = 0; + + lv_draw_rect_dsc_t dot; + lv_draw_rect_dsc_init(&dot); + dot.radius = LV_RADIUS_CIRCLE; + dot.bg_opa = LV_OPA_COVER; + dot.border_width = 0; + dot.bg_color = lv_color_hex(0x0088FF); + + lv_draw_label_dsc_t label; + lv_draw_label_dsc_init(&label); + label.font = font; + label.align = LV_TEXT_ALIGN_CENTER; + label.base.layer = layer; + + static const char* NOTES[12] = {"C","C#","D","D#","E","F","F#","G","G#","A","A#","B"}; + + for (int i = 0; i < 32; i++) { + const auto& s = ts.steps[i]; + int x = ox + i * stepW; + bool isCurrent = (ts.playing && i == ts.currentStep); + + // Row 0: Note name + { + lv_area_t cellArea = { + (lv_coord_t)x, (lv_coord_t)oy, + (lv_coord_t)(x + cellW - 1), (lv_coord_t)(oy + cellH - 1), + }; + cell.bg_color = lv_color_hex(isCurrent ? 0x44BBFF : 0x1A2030); + lv_draw_rect(layer, &cell, &cellArea); + + int midi = tb3StepToMidi(ts, i); + label.text = NOTES[midi % 12]; + label.color = lv_color_hex(isCurrent ? 0x000000 : 0xDDDDDD); + label.ofs_y = (cellH - lineH) / 2; + lv_draw_label(layer, &label, &cellArea); + } + + // Row 1: Oct up/down + { + int y = oy + rowH; + lv_area_t cellArea = { + (lv_coord_t)x, (lv_coord_t)y, + (lv_coord_t)(x + cellW - 1), (lv_coord_t)(y + cellH - 1), + }; + uint32_t bgCol = s.oct_up ? 0x006622 : (s.oct_down ? 0x662200 : 0x111820); + cell.bg_color = lv_color_hex(bgCol); + lv_draw_rect(layer, &cell, &cellArea); + + if (s.oct_up || s.oct_down) { + label.text = s.oct_up ? "+" : "-"; + label.color = lv_color_hex(0xFFFFFF); + label.ofs_y = (cellH - lineH) / 2; + lv_draw_label(layer, &label, &cellArea); + } + } + + // Row 2: Accent / Slide + { + int y = oy + rowH * 2; + lv_area_t cellArea = { + (lv_coord_t)x, (lv_coord_t)y, + (lv_coord_t)(x + cellW - 1), (lv_coord_t)(y + cellH - 1), + }; + uint32_t bgCol = s.accent ? 0x886600 : (s.slide ? 0x004488 : 0x111820); + cell.bg_color = lv_color_hex(bgCol); + lv_draw_rect(layer, &cell, &cellArea); + + if (s.accent || s.slide) { + label.text = (s.accent && s.slide) ? "AS" : (s.accent ? "A" : "S"); + label.color = lv_color_hex(0xFFFFFF); + label.ofs_y = (cellH - lineH) / 2; + lv_draw_label(layer, &label, &cellArea); + } + } + + // Row 3: Gate dot + { + int y = oy + rowH * 3; + lv_area_t cellArea = { + (lv_coord_t)x, (lv_coord_t)y, + (lv_coord_t)(x + cellW - 1), (lv_coord_t)(y + cellH - 1), + }; + cell.bg_color = lv_color_hex(0x111820); + lv_draw_rect(layer, &cell, &cellArea); + + if (s.gate) { + int dotSz = std::min(stepW - 4, rowH - 4); + if (dotSz > 0) { + int dx = x + (cellW - dotSz) / 2; + int dy = y + (cellH - dotSz) / 2; + lv_area_t dotArea = { + (lv_coord_t)dx, (lv_coord_t)dy, + (lv_coord_t)(dx + dotSz - 1), (lv_coord_t)(dy + dotSz - 1), + }; + lv_draw_rect(layer, &dot, &dotArea); + } + } + } + } +} + +static void tb3DrawCanvas(MidiController::Tb3poState& ts) { + if (ts.canvas) lv_obj_invalidate(ts.canvas); +} + +// --------------------------------------------------------------------------- +// Timer callback +// --------------------------------------------------------------------------- + +static void cbTb3poTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + auto& ts = mc->tb3po_; + if (!ts.playing) return; + + uint32_t now = lv_tick_get(); + + // Note-off. A slide flag on the step that owns lastNote holds it into the + // next step, so suppress the normal 90%-gate Note Off until that slide ends. + if (ts.lastNote >= 0 && now >= ts.noteOffMs) { + bool heldBySlide = ts.lastNoteStep >= 0 && ts.steps[ts.lastNoteStep].slide; + if (!heldBySlide) { + mc->sendMidi(0x80, (uint8_t)ts.lastNote, 0); + ts.lastNote = -1; + ts.lastNoteStep = -1; + } + } + + if (now < ts.nextStepMs) return; + + // Advance step + ts.currentStep = (ts.currentStep + 1) % 32; + const auto& s = ts.steps[ts.currentStep]; + + // BPM → 16th note duration + uint32_t stepMs = 60000u / (uint32_t)ts.bpm / 4u; + if (stepMs < 20) stepMs = 20; + ts.nextStepMs = now + stepMs; + + if (s.gate) { + int note = tb3StepToMidi(ts, ts.currentStep); + int prevStep = (ts.currentStep + 31) % 32; + bool incomingSlide = ts.steps[prevStep].slide; + int oldNote = ts.lastNote; + int vel = s.accent ? 127 : 100; + + if (oldNote < 0) { + mc->sendMidi(0x90, (uint8_t)note, (uint8_t)vel); + } else if (incomingSlide && oldNote != note) { + // Start the new pitch while the old one is still on so a mono synth + // hears a legato transition, then release the old pitch. This also + // prevents a poly synth from leaving the old note hanging. + mc->sendMidi(0x90, (uint8_t)note, (uint8_t)vel); + mc->sendMidi(0x80, (uint8_t)oldNote, 0); + } else if (!incomingSlide) { + mc->sendMidi(0x80, (uint8_t)oldNote, 0); + mc->sendMidi(0x90, (uint8_t)note, (uint8_t)vel); + } else { + // Same pitch: retrigger without an intervening Note Off. + mc->sendMidi(0x90, (uint8_t)note, (uint8_t)vel); + } + + ts.lastNote = note; + ts.lastNoteStep = ts.currentStep; + ts.noteOffMs = now + stepMs * 9 / 10; // 90% gate + } else { + if (ts.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)ts.lastNote, 0); + ts.lastNote = -1; + ts.lastNoteStep = -1; + } + } + + tb3DrawCanvas(ts); +} + +// --------------------------------------------------------------------------- +// Callbacks +// --------------------------------------------------------------------------- + +static void cbTb3Back(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbTb3Play(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + ts.playing = !ts.playing; + if (!ts.playing && ts.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)ts.lastNote, 0); + ts.lastNote = -1; + ts.lastNoteStep = -1; + } + if (ts.playing) { + ts.currentStep = 31; // will advance to 0 on first tick + ts.nextStepMs = lv_tick_get(); + } + if (ts.playLbl) lv_label_set_text(ts.playLbl, ts.playing ? "STOP" : "PLAY"); +} + +static void cbTb3BpmDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + if (ts.bpm > 40) ts.bpm -= 5; + if (ts.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", ts.bpm); + lv_label_set_text(ts.bpmLbl, buf); + } +} +static void cbTb3BpmUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + if (ts.bpm < 280) ts.bpm += 5; + if (ts.bpmLbl) { + char buf[8]; snprintf(buf, sizeof(buf), "%d", ts.bpm); + lv_label_set_text(ts.bpmLbl, buf); + } +} + +static void cbTb3DensDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + if (ts.density > 0) ts.density--; + ts.seed = (uint32_t)esp_random(); + tb3Regenerate(ts); + if (ts.densityLbl) { + char buf[6]; snprintf(buf, sizeof(buf), "D:%d", ts.density); + lv_label_set_text(ts.densityLbl, buf); + } + if (ts.seedLbl) { + char buf[16]; snprintf(buf, sizeof(buf), "%08lX", (unsigned long)ts.seed); + lv_label_set_text(ts.seedLbl, buf); + } + tb3DrawCanvas(ts); +} +static void cbTb3DensUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + if (ts.density < 16) ts.density++; + ts.seed = (uint32_t)esp_random(); + tb3Regenerate(ts); + if (ts.densityLbl) { + char buf[6]; snprintf(buf, sizeof(buf), "D:%d", ts.density); + lv_label_set_text(ts.densityLbl, buf); + } + if (ts.seedLbl) { + char buf[16]; snprintf(buf, sizeof(buf), "%08lX", (unsigned long)ts.seed); + lv_label_set_text(ts.seedLbl, buf); + } + tb3DrawCanvas(ts); +} + +static void cbTb3Regen(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + ts.seed = (uint32_t)esp_random(); + tb3Regenerate(ts); + if (ts.seedLbl) { + char buf[16]; snprintf(buf, sizeof(buf), "%08lX", (unsigned long)ts.seed); + lv_label_set_text(ts.seedLbl, buf); + } + tb3DrawCanvas(ts); +} + +static void cbTb3KeyDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + ts.key = (ts.key + 11) % 12; + tb3Regenerate(ts); + if (ts.keyLbl) lv_label_set_text(ts.keyLbl, KEY_NAMES[ts.key]); + tb3DrawCanvas(ts); +} +static void cbTb3KeyUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + ts.key = (ts.key + 1) % 12; + tb3Regenerate(ts); + if (ts.keyLbl) lv_label_set_text(ts.keyLbl, KEY_NAMES[ts.key]); + tb3DrawCanvas(ts); +} + +static void cbTb3ScaleDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + ts.scale = (ts.scale + TB3_NUM_SCALES - 1) % TB3_NUM_SCALES; + tb3Regenerate(ts); + if (ts.scaleLbl) lv_label_set_text(ts.scaleLbl, TB3_SCALE_NAMES[ts.scale]); + tb3DrawCanvas(ts); +} +static void cbTb3ScaleUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& ts = mc->tb3po_; + ts.scale = (ts.scale + 1) % TB3_NUM_SCALES; + tb3Regenerate(ts); + if (ts.scaleLbl) lv_label_set_text(ts.scaleLbl, TB3_SCALE_NAMES[ts.scale]); + tb3DrawCanvas(ts); +} + +// --------------------------------------------------------------------------- +// Build +// --------------------------------------------------------------------------- + +void buildTb3poView(MidiController* mc, lv_obj_t* parent) { + auto& ts = mc->tb3po_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + + int rowH = 88; + int sBtnW = 88; + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x060A0F), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 8, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + auto makeRow = [&]() -> lv_obj_t* { return mcRow(root); }; + + auto makeBtn = [&](lv_obj_t* par, const char* txt, int w, lv_event_cb_t cb, void* ud) -> lv_obj_t* { + return mcSmallBtn(par, txt, w, cb, ud); + }; + + auto makeLbl = [&](lv_obj_t* par, const char* txt, int w, lv_color_t col) -> lv_obj_t* { + return mcValue(par, txt, w, col); + }; + + auto makeSep = [&](lv_obj_t* par) { + lv_obj_t* sep = lv_label_create(par); + lv_label_set_text(sep, "|"); + lv_obj_set_style_text_font(sep, fontSmall, 0); + lv_obj_set_style_text_color(sep, lv_color_hex(0x334455), 0); + }; + + // -- Control row ----------------------------------------------------------- + { + lv_obj_t* row = makeRow(); + mcBackBtn(row, cbTb3Back, mc, MC_W_S); + lv_obj_t* playBtn = makeBtn(row, ts.playing ? "STOP" : "PLAY", 176, cbTb3Play, mc); + ts.playLbl = lv_obj_get_child(playBtn, 0); + makeSep(row); + makeBtn(row, "<", sBtnW, cbTb3BpmDown, mc); + char bpmBuf[8]; snprintf(bpmBuf, sizeof(bpmBuf), "%d", ts.bpm); + ts.bpmLbl = makeLbl(row, bpmBuf, 136, lv_color_hex(0xFFE000)); + makeBtn(row, ">", sBtnW, cbTb3BpmUp, mc); + makeSep(row); + makeBtn(row, "<", sBtnW, cbTb3DensDown, mc); + char densBuf[6]; snprintf(densBuf, sizeof(densBuf), "D:%d", ts.density); + ts.densityLbl = makeLbl(row, densBuf, 112, lv_color_hex(0xFF8844)); + makeBtn(row, ">", sBtnW, cbTb3DensUp, mc); + makeSep(row); + makeBtn(row, "REGEN", 200, cbTb3Regen, mc); + } + + // -- Canvas (flex_grow) ---------------------------------------------------- + { + lv_obj_t* canvasArea = mcContainer(root); + lv_obj_set_width(canvasArea, LV_PCT(100)); + lv_obj_set_flex_grow(canvasArea, 1); + lv_obj_set_style_bg_color(canvasArea, lv_color_hex(0x050810), 0); + lv_obj_set_style_bg_opa(canvasArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(canvasArea, 0, 0); + lv_obj_set_style_pad_all(canvasArea, 0, 0); + lv_obj_set_scrollable(canvasArea, false); + lv_obj_add_event_cb(canvasArea, cbTb3CanvasDraw, LV_EVENT_DRAW_MAIN, mc); + ts.canvas = canvasArea; + } + + // -- Status row: KEY SCALE Seed ----------------------------------------- + { + lv_obj_t* row = makeRow(); + makeBtn(row, "<", sBtnW, cbTb3KeyDown, mc); + ts.keyLbl = makeLbl(row, KEY_NAMES[ts.key], 88, lv_color_hex(0x88DDFF)); + makeBtn(row, ">", sBtnW, cbTb3KeyUp, mc); + makeSep(row); + makeBtn(row, "<", sBtnW, cbTb3ScaleDown, mc); + ts.scaleLbl = makeLbl(row, TB3_SCALE_NAMES[ts.scale], 176, lv_color_hex(0xAAFF88)); + makeBtn(row, ">", sBtnW, cbTb3ScaleUp, mc); + makeSep(row); + lv_obj_t* seedHdr = lv_label_create(row); + lv_label_set_text(seedHdr, "Seed:"); + lv_obj_set_style_text_font(seedHdr, fontSmall, 0); + lv_obj_set_style_text_color(seedHdr, lv_color_hex(0x666688), 0); + char seedBuf[16]; snprintf(seedBuf, sizeof(seedBuf), "%08lX", (unsigned long)ts.seed); + ts.seedLbl = makeLbl(row, seedBuf, 256, lv_color_hex(0x8888AA)); + } + + // -- Initial pattern + draw ------------------------------------------------- + if (ts.seed == 0) ts.seed = (uint32_t)esp_random(); + tb3Regenerate(ts); + + // Start timer + mc->tb3poTimer_ = lv_timer_create(cbTb3poTimer, 10, mc); + ts.nextStepMs = lv_tick_get(); + tb3DrawCanvas(ts); +} + +// --------------------------------------------------------------------------- +// Public encoder entry points (called from EncoderInput.cpp) +// --------------------------------------------------------------------------- + +void tb3TogglePlay(MidiController* mc) { + auto& ts = mc->tb3po_; + ts.playing = !ts.playing; + if (!ts.playing && ts.lastNote >= 0) { + mc->sendMidi(0x80, (uint8_t)ts.lastNote, 0); + ts.lastNote = -1; + ts.lastNoteStep = -1; + } + if (ts.playing) { + ts.currentStep = 31; + ts.nextStepMs = lv_tick_get(); + } + if (ts.playLbl) lv_label_set_text(ts.playLbl, ts.playing ? "STOP" : "PLAY"); +} + +void tb3ChangeScale(MidiController* mc, int delta) { + auto& ts = mc->tb3po_; + static constexpr int NUM_SCALES = 6; + ts.scale = ((ts.scale + delta) % NUM_SCALES + NUM_SCALES) % NUM_SCALES; + tb3Regenerate(ts); + if (ts.scaleLbl) lv_label_set_text(ts.scaleLbl, TB3_SCALE_NAMES[ts.scale]); + tb3DrawCanvas(ts); +} + +void tb3Regen(MidiController* mc) { + auto& ts = mc->tb3po_; + ts.seed = (uint32_t)esp_random(); + tb3Regenerate(ts); + if (ts.seedLbl) { + char buf[16]; snprintf(buf, sizeof(buf), "%08lX", (unsigned long)ts.seed); + lv_label_set_text(ts.seedLbl, buf); + } + tb3DrawCanvas(ts); +} + +void tb3Redraw(MidiController* mc) { + if (mc) tb3DrawCanvas(mc->tb3po_); +} diff --git a/Apps/MidiController/main/Source/ViewXYPad.cpp b/Apps/MidiController/main/Source/ViewXYPad.cpp new file mode 100644 index 0000000..b1c2b01 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewXYPad.cpp @@ -0,0 +1,310 @@ +#include "MidiController.h" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// XY PAD view - 2D touch pad for MIDI CC modulation +// +// Layout (flex column): +// [header: < | XY PAD | X CC -/val/+ | | Y CC -/val/+] +// [touch area - flex_grow, LV_EVENT_DRAW_MAIN] +// [value readout footer] +// --------------------------------------------------------------------------- + + +static constexpr int XY_IND_RADIUS = 32; // indicator circle radius (pixels) + +// --------------------------------------------------------------------------- +// Draw event - renders BG, crosshair, indicator circle +// --------------------------------------------------------------------------- + +static void cbXYDraw(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + + lv_obj_t* obj = lv_event_get_target_obj(e); + lv_layer_t* layer = lv_event_get_layer(e); + + lv_area_t area; + lv_obj_get_coords(obj, &area); + int32_t ox = area.x1, oy = area.y1; + int32_t cW = lv_area_get_width(&area); + int32_t cH = lv_area_get_height(&area); + + // Background + lv_draw_fill_dsc_t bg; + lv_draw_fill_dsc_init(&bg); + bg.color = lv_color_hex(0x080C12); + bg.opa = LV_OPA_COVER; + lv_draw_fill(layer, &bg, &area); + + // Crosshair lines + lv_draw_line_dsc_t line; + lv_draw_line_dsc_init(&line); + line.color = lv_color_hex(0x1A3040); + line.width = 1; + line.opa = LV_OPA_COVER; + + // Horizontal + line.p1.x = (lv_value_precise_t)ox; + line.p1.y = (lv_value_precise_t)(oy + cH / 2); + line.p2.x = (lv_value_precise_t)(ox + cW - 1); + line.p2.y = (lv_value_precise_t)(oy + cH / 2); + lv_draw_line(layer, &line); + + // Vertical + line.p1.x = (lv_value_precise_t)(ox + cW / 2); + line.p1.y = (lv_value_precise_t)oy; + line.p2.x = (lv_value_precise_t)(ox + cW / 2); + line.p2.y = (lv_value_precise_t)(oy + cH - 1); + lv_draw_line(layer, &line); + +} + +static void updateValLabel(MidiController* mc) { + if (!mc->xy_.valLbl) return; + char buf[24]; + snprintf(buf, sizeof(buf), "X:%3d Y:%3d", mc->xy_.xVal, mc->xy_.yVal); + lv_label_set_text(mc->xy_.valLbl, buf); +} + +// --------------------------------------------------------------------------- +// Touch input on canvas +// --------------------------------------------------------------------------- + +static void cbCanvasTouch(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& xy = mc->xy_; + if (xy.canvasW <= 1 || xy.canvasH <= 1) return; + + lv_indev_t* indev = lv_event_get_indev(e); + if (!indev) return; + lv_point_t pt; + lv_indev_get_point(indev, &pt); + + lv_area_t area; + lv_obj_get_coords(xy.canvas, &area); + int lx = (int)pt.x - (int)area.x1; + int ly = (int)pt.y - (int)area.y1; + lx = lx < 0 ? 0 : (lx >= xy.canvasW ? xy.canvasW - 1 : lx); + ly = ly < 0 ? 0 : (ly >= xy.canvasH ? xy.canvasH - 1 : ly); + + int newX = (lx * 127) / (xy.canvasW - 1); + int newY = 127 - (ly * 127) / (xy.canvasH - 1); + + bool changed = (newX != xy.xVal) || (newY != xy.yVal); + if (!changed) return; + + xy.xVal = newX; + xy.yVal = newY; + + mc->sendMidi(0xB0, (uint8_t)xy.xCC, (uint8_t)xy.xVal); + mc->sendMidi(0xB0, (uint8_t)xy.yCC, (uint8_t)xy.yVal); + + // Move indicator widget immediately + if (xy.indicator && xy.canvasW > 0 && xy.canvasH > 0) { + int indR = XY_IND_RADIUS; + int px = (xy.xVal * (xy.canvasW - 1)) / 127; + int py = ((127 - xy.yVal) * (xy.canvasH - 1)) / 127; + lv_obj_set_pos(xy.indicator, (lv_coord_t)(px - indR), (lv_coord_t)(py - indR)); + } + + lv_obj_invalidate(xy.canvas); + updateValLabel(mc); +} + +static void cbCanvasSize(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& xy = mc->xy_; + lv_obj_t* obj = lv_event_get_target_obj(e); + xy.canvasW = (int)lv_obj_get_content_width(obj); + xy.canvasH = (int)lv_obj_get_content_height(obj); + // Place indicator at current value position + if (xy.indicator && xy.canvasW > 0 && xy.canvasH > 0) { + int indR = XY_IND_RADIUS; + int px = (xy.xVal * (xy.canvasW - 1)) / 127; + int py = ((127 - xy.yVal) * (xy.canvasH - 1)) / 127; + lv_obj_set_pos(xy.indicator, (lv_coord_t)(px - indR), (lv_coord_t)(py - indR)); + } +} + +// --------------------------------------------------------------------------- +// CC control callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbXCCDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->xy_.xCC = (mc->xy_.xCC - 1 + 128) % 128; + if (mc->xy_.xCCLbl) { + char buf[12]; snprintf(buf, sizeof(buf), "X CC:%d", mc->xy_.xCC); + lv_label_set_text(mc->xy_.xCCLbl, buf); + } +} +static void cbXCCUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->xy_.xCC = (mc->xy_.xCC + 1) % 128; + if (mc->xy_.xCCLbl) { + char buf[12]; snprintf(buf, sizeof(buf), "X CC:%d", mc->xy_.xCC); + lv_label_set_text(mc->xy_.xCCLbl, buf); + } +} +static void cbYCCDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->xy_.yCC = (mc->xy_.yCC - 1 + 128) % 128; + if (mc->xy_.yCCLbl) { + char buf[12]; snprintf(buf, sizeof(buf), "Y CC:%d", mc->xy_.yCC); + lv_label_set_text(mc->xy_.yCCLbl, buf); + } +} +static void cbYCCUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->xy_.yCC = (mc->xy_.yCC + 1) % 128; + if (mc->xy_.yCCLbl) { + char buf[12]; snprintf(buf, sizeof(buf), "Y CC:%d", mc->xy_.yCC); + lv_label_set_text(mc->xy_.yCCLbl, buf); + } +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildXYPadView(MidiController* mc, lv_obj_t* parent) { + auto& xy = mc->xy_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + const lv_font_t* fontNormal = lvgl_get_text_font(FONT_SIZE_DEFAULT); + + int hdrH = 88; + int footH = 64; + + lv_obj_t* root = mcContainer(parent); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // -- Header ---------------------------------------------------------------- + lv_obj_t* hdr = mcRow(root, hdrH); + + int btnH = MC_BTN_H; + + mcBackBtn(hdr, cbBack, mc, MC_W_S); + + lv_obj_t* titleLbl = lv_label_create(hdr); + lv_label_set_text(titleLbl, "XY PAD"); + lv_obj_set_style_text_font(titleLbl, fontNormal, 0); + lv_obj_set_style_text_color(titleLbl, lv_color_hex(0x00DFFF), 0); + lv_obj_set_flex_grow(titleLbl, 1); + + // X CC controls + lv_obj_t* xDown = mcButton(hdr); + lv_obj_set_size(xDown, 72, btnH); + lv_obj_add_event_cb(xDown, cbXCCDown, LV_EVENT_CLICKED, mc); + lv_obj_t* xdl = lv_label_create(xDown); lv_label_set_text(xdl, "-"); + lv_obj_set_style_text_font(xdl, fontSmall, 0); lv_obj_center(xdl); + + xy.xCCLbl = lv_label_create(hdr); + char xbuf[12]; snprintf(xbuf, sizeof(xbuf), "X CC:%d", xy.xCC); + lv_label_set_text(xy.xCCLbl, xbuf); + lv_obj_set_style_text_font(xy.xCCLbl, fontSmall, 0); + lv_obj_set_style_text_color(xy.xCCLbl, lv_color_hex(0x00DFFF), 0); + lv_obj_set_width(xy.xCCLbl, 128); + lv_obj_set_style_text_align(xy.xCCLbl, LV_TEXT_ALIGN_CENTER, 0); + + lv_obj_t* xUp = mcButton(hdr); + lv_obj_set_size(xUp, 72, btnH); + lv_obj_add_event_cb(xUp, cbXCCUp, LV_EVENT_CLICKED, mc); + lv_obj_t* xul = lv_label_create(xUp); lv_label_set_text(xul, "+"); + lv_obj_set_style_text_font(xul, fontSmall, 0); lv_obj_center(xul); + + lv_obj_t* divLbl = lv_label_create(hdr); + lv_label_set_text(divLbl, "|"); + lv_obj_set_style_text_color(divLbl, lv_color_hex(0x334455), 0); + + // Y CC controls + lv_obj_t* yDown = mcButton(hdr); + lv_obj_set_size(yDown, 72, btnH); + lv_obj_add_event_cb(yDown, cbYCCDown, LV_EVENT_CLICKED, mc); + lv_obj_t* ydl = lv_label_create(yDown); lv_label_set_text(ydl, "-"); + lv_obj_set_style_text_font(ydl, fontSmall, 0); lv_obj_center(ydl); + + xy.yCCLbl = lv_label_create(hdr); + char ybuf[12]; snprintf(ybuf, sizeof(ybuf), "Y CC:%d", xy.yCC); + lv_label_set_text(xy.yCCLbl, ybuf); + lv_obj_set_style_text_font(xy.yCCLbl, fontSmall, 0); + lv_obj_set_style_text_color(xy.yCCLbl, lv_color_hex(0xFF8000), 0); + lv_obj_set_width(xy.yCCLbl, 128); + lv_obj_set_style_text_align(xy.yCCLbl, LV_TEXT_ALIGN_CENTER, 0); + + lv_obj_t* yUp = mcButton(hdr); + lv_obj_set_size(yUp, 72, btnH); + lv_obj_add_event_cb(yUp, cbYCCUp, LV_EVENT_CLICKED, mc); + lv_obj_t* yul = lv_label_create(yUp); lv_label_set_text(yul, "+"); + lv_obj_set_style_text_font(yul, fontSmall, 0); lv_obj_center(yul); + + // -- Touch/Draw area ------------------------------------------------------- + lv_obj_t* padObj = mcContainer(root); + lv_obj_set_width(padObj, LV_PCT(100)); + lv_obj_set_flex_grow(padObj, 1); + lv_obj_set_style_bg_color(padObj, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(padObj, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(padObj, 0, 0); + lv_obj_set_style_pad_all(padObj, 0, 0); + lv_obj_set_scrollable(padObj, false); + lv_obj_set_clickable(padObj, true); + lv_obj_add_event_cb(padObj, cbXYDraw, LV_EVENT_DRAW_MAIN, mc); + lv_obj_add_event_cb(padObj, cbCanvasSize, LV_EVENT_SIZE_CHANGED, mc); + lv_obj_add_event_cb(padObj, cbCanvasTouch, LV_EVENT_PRESSING, mc); + xy.canvas = padObj; + + // Indicator circle widget (positioned absolutely within canvas) + int indR = XY_IND_RADIUS; + lv_obj_t* ind = mcContainer(padObj); + lv_obj_set_size(ind, indR * 2, indR * 2); + lv_obj_set_style_radius(ind, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(ind, lv_color_hex(0x00DFFF), 0); + lv_obj_set_style_bg_opa(ind, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(ind, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_border_width(ind, 4, 0); + lv_obj_set_style_pad_all(ind, 0, 0); + lv_obj_set_scrollable(ind, false); + lv_obj_set_clickable(ind, false); + lv_obj_set_pos(ind, 0, 0); // will be updated on first draw/touch + xy.indicator = ind; + + // -- Footer: value readout ------------------------------------------------ + lv_obj_t* footer = mcContainer(root); + lv_obj_set_size(footer, LV_PCT(100), footH); + lv_obj_set_style_bg_color(footer, lv_color_hex(0x0A0C14), 0); + lv_obj_set_style_bg_opa(footer, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(footer, 0, 0); + lv_obj_set_style_pad_all(footer, 8, 0); + lv_obj_set_scrollable(footer, false); + + xy.valLbl = lv_label_create(footer); + lv_label_set_text(xy.valLbl, "X: 64 Y: 64"); + lv_obj_set_style_text_font(xy.valLbl, fontSmall, 0); + lv_obj_set_style_text_color(xy.valLbl, lv_color_hex(0x666688), 0); + lv_obj_center(xy.valLbl); + + updateValLabel(mc); +} diff --git a/Apps/MidiController/main/Source/ViewZen.cpp b/Apps/MidiController/main/Source/ViewZen.cpp new file mode 100644 index 0000000..15cfc08 --- /dev/null +++ b/Apps/MidiController/main/Source/ViewZen.cpp @@ -0,0 +1,521 @@ +#include "MidiController.h" + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// ZEN view - Ambient bouncing ball physics + note triggering +// +// Layout (flex column): +// [physics area - flex_grow, LV_EVENT_DRAW_MAIN] +// [control row: < | ADD | RESET | SCALE | KEY- | KEY+ | OCT] +// --------------------------------------------------------------------------- + + +static constexpr int TOP_SEGS = 8; +static constexpr int BOTTOM_SEGS = 8; +static constexpr int SIDE_SEGS = 4; +static constexpr int TOTAL_WALL_SEGS = TOP_SEGS + BOTTOM_SEGS + SIDE_SEGS * 2; + +struct WallSeg { + float x1, y1, x2, y2; // normalized 0..1 + int noteIdx; + int scaleDegree; + int octaveOffset; +}; + +static void buildWalls(WallSeg walls[]) { + float segW = 1.0f / TOP_SEGS; + for (int i = 0; i < TOP_SEGS; i++) { + walls[i] = { i * segW, 0.0f, (i+1) * segW, 0.0f, i, i, 0 }; + } + for (int i = 0; i < BOTTOM_SEGS; i++) { + walls[TOP_SEGS + i] = { i * segW, 1.0f, (i+1) * segW, 1.0f, + TOP_SEGS + i, BOTTOM_SEGS - 1 - i, 0 }; + } + float segH = 1.0f / SIDE_SEGS; + for (int i = 0; i < SIDE_SEGS; i++) { + int idx = TOP_SEGS + BOTTOM_SEGS + i; + walls[idx] = { 0.0f, i * segH, 0.0f, (i+1) * segH, idx, i, 1 }; + } + for (int i = 0; i < SIDE_SEGS; i++) { + int idx = TOP_SEGS + BOTTOM_SEGS + SIDE_SEGS + i; + walls[idx] = { 1.0f, i * segH, 1.0f, (i+1) * segH, + idx, SIDE_SEGS - 1 - i, 1 }; + } +} + +static const WallSeg* getWalls() { + static WallSeg s_walls[TOTAL_WALL_SEGS]; + static bool s_built = false; + if (!s_built) { + buildWalls(s_walls); + s_built = true; + } + return s_walls; +} + +static constexpr uint32_t ZEN_COLORS[] = { + 0xFF4444, 0xFF8800, 0xFFFF00, 0x44FF44, +}; + +// The ambient mode reads better slightly slower than the original tuning. +static constexpr float ZEN_SPEED_FACTOR = 0.90f; + +// Tracks whether any wall is currently flashing, so the canvas can be fully +// repainted once when the last flash ends (without repainting it every frame). +static bool s_zenFlashActive = false; + +// --------------------------------------------------------------------------- +// Shared first-ball initializer (called from cbCanvasSize and cbZenTimer fallback) +// --------------------------------------------------------------------------- + +// Note: does NOT update ballCntLbl - callers are responsible for label refresh. +// cbZenTimer updates it explicitly; cbCanvasSize relies on buildZenView initializing it to "Balls: x1". +static void initFirstBall(MidiController::ZenState& zn, int w, int h) { + float speedScale = (float)w / 220.0f; + zn.balls[0].x = w * 0.3f; + zn.balls[0].y = h * 0.4f; + zn.balls[0].vx = 2.0f * speedScale * ZEN_SPEED_FACTOR; + zn.balls[0].vy = 1.5f * speedScale * ZEN_SPEED_FACTOR; + zn.balls[0].radius = 20; + zn.balls[0].color = ZEN_COLORS[0]; + zn.balls[0].active = true; + zn.numBalls = 1; +} + +// --------------------------------------------------------------------------- +// Draw event - draws background and wall segments only; balls are lv_obj_t children +// --------------------------------------------------------------------------- + +static void cbZenDraw(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& zn = mc->zen_; + + lv_obj_t* obj = lv_event_get_target_obj(e); + lv_layer_t* layer = lv_event_get_layer(e); + + lv_area_t area; + lv_obj_get_coords(obj, &area); + int32_t ox = area.x1, oy = area.y1; + int32_t cW = lv_area_get_width(&area); + int32_t cH = lv_area_get_height(&area); + + // Update dimensions only if they look valid (never write 0 to physics state) + if (cW > 0 && cH > 0) { + zn.canvasW = (int)cW; + zn.canvasH = (int)cH; + } + + // Background fill + lv_draw_fill_dsc_t bg; + lv_draw_fill_dsc_init(&bg); + bg.color = lv_color_hex(0x080C12); + bg.opa = LV_OPA_COVER; + lv_draw_fill(layer, &bg, &area); + + uint32_t nowMs = lv_tick_get(); + + const WallSeg* walls = getWalls(); + + // Draw wall segments + lv_draw_line_dsc_t line; + lv_draw_line_dsc_init(&line); + line.width = 12; + line.opa = LV_OPA_COVER; + + for (int w = 0; w < TOTAL_WALL_SEGS; w++) { + bool flashing = (nowMs < zn.wallFlashUntilMs[w]); + line.color = flashing ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x1A3050); + line.p1.x = ox + (lv_value_precise_t)(walls[w].x1 * (float)(cW - 1)); + line.p1.y = oy + (lv_value_precise_t)(walls[w].y1 * (float)(cH - 1)); + line.p2.x = ox + (lv_value_precise_t)(walls[w].x2 * (float)(cW - 1)); + line.p2.y = oy + (lv_value_precise_t)(walls[w].y2 * (float)(cH - 1)); + lv_draw_line(layer, &line); + } +} + +// --------------------------------------------------------------------------- +// Physics timer - updates state, invalidates object to trigger redraw +// --------------------------------------------------------------------------- + +// Create or show a ball widget for slot b +static void ensureBallWidget(MidiController* mc, int b) { + auto& zn = mc->zen_; + if (zn.ballObj[b]) return; + int r = zn.balls[b].radius; + lv_obj_t* w = mcContainer(zn.canvas); + lv_obj_set_size(w, r * 2, r * 2); + lv_obj_set_style_radius(w, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(w, lv_color_hex(ZEN_COLORS[b % 4]), 0); + lv_obj_set_style_bg_opa(w, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(w, 0, 0); + lv_obj_set_style_pad_all(w, 0, 0); + lv_obj_set_scrollable(w, false); + lv_obj_set_clickable(w, false); + lv_obj_set_pos(w, (lv_coord_t)(zn.balls[b].x - r), (lv_coord_t)(zn.balls[b].y - r)); + zn.ballObj[b] = w; +} + +static void scheduleZenNoteOff(MidiController* mc, uint8_t note, uint32_t delayMs) { + auto& zn = mc->zen_; + uint32_t offAt = lv_tick_get() + delayMs; + // Find a free slot + for (int i = 0; i < MidiController::ZenState::MAX_PENDING_OFFS; i++) { + if (zn.pendingOffMs[i] == 0) { + zn.pendingOffNote[i] = note; + zn.pendingOffMs[i] = offAt ? offAt : 1; // avoid 0 sentinel + return; + } + } + // No free slot - send immediately as fallback + mc->sendMidi(0x80, note, 0); +} + +static void cbZenTimer(lv_timer_t* t) { + auto* mc = static_cast(lv_timer_get_user_data(t)); + if (!mc) return; + auto& zn = mc->zen_; + lv_obj_t* canvas = zn.canvas; // snapshot - must not use zn.canvas after ensureBallWidget + if (!canvas || zn.canvasW <= 0 || zn.canvasH <= 0) return; + + // Process pending note-offs + uint32_t nowMs = lv_tick_get(); + for (int i = 0; i < MidiController::ZenState::MAX_PENDING_OFFS; i++) { + if (zn.pendingOffMs[i] != 0 && (int32_t)(nowMs - zn.pendingOffMs[i]) >= 0) { + mc->sendMidi(0x80, zn.pendingOffNote[i], 0); + zn.pendingOffMs[i] = 0; + } + } + + // Fallback: initialize first ball if SIZE_CHANGED fired before state was ready + if (zn.numBalls == 0) { + initFirstBall(zn, zn.canvasW, zn.canvasH); + if (zn.ballCntLbl) lv_label_set_text(zn.ballCntLbl, "Balls: x1"); + } + + float cW = (float)zn.canvasW; + float cH = (float)zn.canvasH; + float minSpd = 1.5f * cW / 220.0f * ZEN_SPEED_FACTOR; + + const WallSeg* walls = getWalls(); + + for (int b = 0; b < MidiController::ZenState::MAX_BALLS; b++) { + auto& ball = zn.balls[b]; + if (!ball.active) continue; + + ball.x += ball.vx; + ball.y += ball.vy; + + // Top bounce + if (ball.y - ball.radius <= 0) { + ball.y = (float)ball.radius; + ball.vy = fabsf(ball.vy); + if (fabsf(ball.vy) < minSpd) ball.vy = minSpd; + float fx = ball.x / cW; + int seg = (int)(fx * TOP_SEGS); + seg = seg < 0 ? 0 : (seg >= TOP_SEGS ? TOP_SEGS - 1 : seg); + int note = std::max(0, std::min(127, getNoteInScale(zn.scale, walls[seg].scaleDegree, zn.octave + walls[seg].octaveOffset, zn.key))); + mc->sendMidi(0x90, (uint8_t)note, 70 + (int)(esp_random() % 40)); + scheduleZenNoteOff(mc, (uint8_t)note, 150); + zn.wallFlashUntilMs[walls[seg].noteIdx] = nowMs + 200; + lv_obj_invalidate(canvas); + } + // Bottom bounce + if (ball.y + ball.radius >= cH) { + ball.y = cH - ball.radius; + ball.vy = -fabsf(ball.vy); + if (fabsf(ball.vy) < minSpd) ball.vy = -minSpd; + float fx = ball.x / cW; + int seg = (int)(fx * BOTTOM_SEGS); + seg = seg < 0 ? 0 : (seg >= BOTTOM_SEGS ? BOTTOM_SEGS - 1 : seg); + int wallIdx = TOP_SEGS + seg; + int note = std::max(0, std::min(127, getNoteInScale(zn.scale, walls[wallIdx].scaleDegree, zn.octave + walls[wallIdx].octaveOffset, zn.key))); + mc->sendMidi(0x90, (uint8_t)note, 70 + (int)(esp_random() % 40)); + scheduleZenNoteOff(mc, (uint8_t)note, 150); + zn.wallFlashUntilMs[walls[wallIdx].noteIdx] = nowMs + 200; + lv_obj_invalidate(canvas); + } + // Left bounce + if (ball.x - ball.radius <= 0) { + ball.x = (float)ball.radius; + ball.vx = fabsf(ball.vx); + if (fabsf(ball.vx) < minSpd) ball.vx = minSpd; + float fy = ball.y / cH; + int seg = (int)(fy * SIDE_SEGS); + seg = seg < 0 ? 0 : (seg >= SIDE_SEGS ? SIDE_SEGS - 1 : seg); + int wallIdx = TOP_SEGS + BOTTOM_SEGS + seg; + int note = std::max(0, std::min(127, getNoteInScale(zn.scale, walls[wallIdx].scaleDegree, zn.octave + walls[wallIdx].octaveOffset, zn.key))); + mc->sendMidi(0x90, (uint8_t)note, 70 + (int)(esp_random() % 40)); + scheduleZenNoteOff(mc, (uint8_t)note, 150); + zn.wallFlashUntilMs[walls[wallIdx].noteIdx] = nowMs + 200; + lv_obj_invalidate(canvas); + } + // Right bounce + if (ball.x + ball.radius >= cW) { + ball.x = cW - ball.radius; + ball.vx = -fabsf(ball.vx); + if (fabsf(ball.vx) < minSpd) ball.vx = -minSpd; + float fy = ball.y / cH; + int seg = (int)(fy * SIDE_SEGS); + seg = seg < 0 ? 0 : (seg >= SIDE_SEGS ? SIDE_SEGS - 1 : seg); + int wallIdx = TOP_SEGS + BOTTOM_SEGS + SIDE_SEGS + seg; + int note = std::max(0, std::min(127, getNoteInScale(zn.scale, walls[wallIdx].scaleDegree, zn.octave + walls[wallIdx].octaveOffset, zn.key))); + mc->sendMidi(0x90, (uint8_t)note, 70 + (int)(esp_random() % 40)); + scheduleZenNoteOff(mc, (uint8_t)note, 150); + zn.wallFlashUntilMs[walls[wallIdx].noteIdx] = nowMs + 200; + lv_obj_invalidate(canvas); + } + } + + // Update ball widget positions (create if needed, hide if inactive) + for (int b = 0; b < MidiController::ZenState::MAX_BALLS; b++) { + auto& ball = zn.balls[b]; + if (ball.active) { + ensureBallWidget(mc, b); + int r = ball.radius; + lv_obj_set_pos(zn.ballObj[b], (lv_coord_t)(ball.x - r), (lv_coord_t)(ball.y - r)); + lv_obj_set_hidden(zn.ballObj[b], false); + } else if (zn.ballObj[b]) { + lv_obj_set_hidden(zn.ballObj[b], true); + } + } + + // Moving the ball widgets already invalidates their old and new areas, so + // the canvas only needs a full repaint when a wall flash starts (handled at + // collision time) or when the last flash expires. + bool anyFlash = false; + for (int w = 0; w < TOTAL_WALL_SEGS; w++) { + if (nowMs < zn.wallFlashUntilMs[w]) { anyFlash = true; break; } + } + if (s_zenFlashActive && !anyFlash && canvas && canvas == zn.canvas) { + lv_obj_invalidate(canvas); + } + s_zenFlashActive = anyFlash; +} + +// --------------------------------------------------------------------------- +// Canvas size event - initialize balls after first layout +// --------------------------------------------------------------------------- + +static void cbCanvasSize(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& zn = mc->zen_; + lv_obj_t* obj = lv_event_get_target_obj(e); + int w = (int)lv_obj_get_content_width(obj); + int h = (int)lv_obj_get_content_height(obj); + if (w <= 0 || h <= 0) return; + + zn.canvasW = w; + zn.canvasH = h; + + // Initialize first ball on first valid layout + if (zn.numBalls == 0) { + initFirstBall(zn, w, h); + } +} + +// --------------------------------------------------------------------------- +// Control callbacks +// --------------------------------------------------------------------------- + +static void cbBack(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (mc) mc->switchToMode(MidiMode::HOME); +} + +static void cbAddBall(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& zn = mc->zen_; + if (zn.numBalls >= MidiController::ZenState::MAX_BALLS) return; + + int b = zn.numBalls; + zn.balls[b].x = zn.canvasW * 0.3f + (float)(esp_random() % 100) * 0.01f * zn.canvasW * 0.4f; + zn.balls[b].y = zn.canvasH * 0.3f + (float)(esp_random() % 100) * 0.01f * zn.canvasH * 0.4f; + float speedScale = (float)zn.canvasW / 220.0f; + float spd = (1.5f + (float)(esp_random() % 14) * 0.1f) * speedScale * ZEN_SPEED_FACTOR; + float ang = (float)(esp_random() % 628) * 0.01f; + zn.balls[b].vx = cosf(ang) * spd; + zn.balls[b].vy = sinf(ang) * spd; + zn.balls[b].radius = 20; + zn.balls[b].color = ZEN_COLORS[b % 4]; + zn.balls[b].active = true; + zn.numBalls++; + if (zn.ballCntLbl) { + char buf[24]; snprintf(buf, sizeof(buf), "Balls: x%d", zn.numBalls); + lv_label_set_text(zn.ballCntLbl, buf); + } +} + +static void cbReset(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& zn = mc->zen_; + for (int b = 1; b < MidiController::ZenState::MAX_BALLS; b++) zn.balls[b].active = false; + zn.numBalls = 1; + if (zn.ballCntLbl) lv_label_set_text(zn.ballCntLbl, "Balls: x1"); +} + +static void cbScaleCycle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->zen_.scale = (mc->zen_.scale + 1) % NUM_MIDI_SCALES; + if (mc->zen_.statusLbl) { + char buf[24]; + snprintf(buf, sizeof(buf), "%s %s", KEY_NAMES[mc->zen_.key], MIDI_SCALES[mc->zen_.scale].name); + lv_label_set_text(mc->zen_.statusLbl, buf); + } +} + +static void cbKeyDown(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->zen_.key = (mc->zen_.key + 11) % 12; + if (mc->zen_.statusLbl) { + char buf[24]; + snprintf(buf, sizeof(buf), "%s %s", KEY_NAMES[mc->zen_.key], MIDI_SCALES[mc->zen_.scale].name); + lv_label_set_text(mc->zen_.statusLbl, buf); + } +} +static void cbKeyUp(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->zen_.key = (mc->zen_.key + 1) % 12; + if (mc->zen_.statusLbl) { + char buf[24]; + snprintf(buf, sizeof(buf), "%s %s", KEY_NAMES[mc->zen_.key], MIDI_SCALES[mc->zen_.scale].name); + lv_label_set_text(mc->zen_.statusLbl, buf); + } +} + +static void cbOctCycle(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + mc->zen_.octave++; + if (mc->zen_.octave > 6) mc->zen_.octave = 2; + if (mc->zen_.octLbl) { + char buf[6]; snprintf(buf, sizeof(buf), "O:%d", mc->zen_.octave); + lv_label_set_text(mc->zen_.octLbl, buf); + } +} + +// --------------------------------------------------------------------------- +// Teardown +// --------------------------------------------------------------------------- + +static void cbZenRootDelete(lv_event_t* e) { + auto* mc = static_cast(lv_event_get_user_data(e)); + if (!mc) return; + auto& zn = mc->zen_; + + // Stop and delete the zen timer so it cannot fire after widgets are gone + if (mc->zenTimer_) { + lv_timer_delete(mc->zenTimer_); + mc->zenTimer_ = nullptr; + } + + // Flush all pending note-offs immediately so no notes get stuck + for (int i = 0; i < MidiController::ZenState::MAX_PENDING_OFFS; i++) { + if (zn.pendingOffMs[i] != 0) { + mc->sendMidi(0x80, zn.pendingOffNote[i], 0); + zn.pendingOffMs[i] = 0; + } + } + + // Null all stored LVGL pointers + zn.canvas = nullptr; + zn.statusLbl = nullptr; + zn.ballCntLbl = nullptr; + zn.octLbl = nullptr; + for (int b = 0; b < MidiController::ZenState::MAX_BALLS; b++) + zn.ballObj[b] = nullptr; +} + +// --------------------------------------------------------------------------- +// Build function +// --------------------------------------------------------------------------- + +void buildZenView(MidiController* mc, lv_obj_t* parent) { + auto& zn = mc->zen_; + + const lv_font_t* fontSmall = lvgl_get_text_font(FONT_SIZE_SMALL); + int ctrlH = 88; + int btnH = MC_BTN_H; + + lv_obj_t* root = mcContainer(parent); + lv_obj_add_event_cb(root, cbZenRootDelete, LV_EVENT_DELETE, mc); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_color(root, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(root, 0, 0); + lv_obj_set_style_pad_all(root, 0, 0); + lv_obj_set_style_pad_gap(root, 0, 0); + lv_obj_set_scrollable(root, false); + mc->currentViewRoot_ = root; + + // -- Control row (TOP) ---------------------------------------------------- + lv_obj_t* ctrl = mcRow(root, ctrlH); + + auto makeCtrlBtn = [&](const char* lbl, lv_event_cb_t cb) { + lv_obj_t* btn = mcButton(ctrl); + lv_obj_set_size(btn, 112, btnH); + lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, mc); + lv_obj_t* l = lv_label_create(btn); + lv_label_set_text(l, lbl); + lv_obj_set_style_text_font(l, fontSmall, 0); + lv_obj_center(l); + }; + + mcBackBtn(ctrl, cbBack, mc, MC_W_XS); + + makeCtrlBtn("ADD", cbAddBall); + makeCtrlBtn("RESET", cbReset); + makeCtrlBtn("SCALE", cbScaleCycle); + makeCtrlBtn("KEY-", cbKeyDown); + makeCtrlBtn("KEY+", cbKeyUp); + makeCtrlBtn("OCT", cbOctCycle); + + zn.octLbl = lv_label_create(ctrl); + { char buf[6]; snprintf(buf, sizeof(buf), "O:%d", zn.octave); lv_label_set_text(zn.octLbl, buf); } + lv_obj_set_style_text_font(zn.octLbl, fontSmall, 0); + lv_obj_set_style_text_color(zn.octLbl, lv_color_hex(0x00DFFF), 0); + lv_obj_set_width(zn.octLbl, 112); + lv_obj_set_style_text_align(zn.octLbl, LV_TEXT_ALIGN_CENTER, 0); + + zn.ballCntLbl = lv_label_create(ctrl); + lv_label_set_text(zn.ballCntLbl, "Balls: x1"); + lv_obj_set_style_text_font(zn.ballCntLbl, fontSmall, 0); + lv_obj_set_style_text_color(zn.ballCntLbl, lv_color_hex(0x666688), 0); + + zn.statusLbl = lv_label_create(ctrl); + { char sbuf[24]; snprintf(sbuf, sizeof(sbuf), "%s %s", KEY_NAMES[zn.key], MIDI_SCALES[zn.scale].name); + lv_label_set_text(zn.statusLbl, sbuf); } + lv_obj_set_style_text_font(zn.statusLbl, fontSmall, 0); + lv_obj_set_style_text_color(zn.statusLbl, lv_color_hex(0x00DFFF), 0); + lv_obj_set_flex_grow(zn.statusLbl, 1); + lv_obj_set_style_text_align(zn.statusLbl, LV_TEXT_ALIGN_RIGHT, 0); + + // -- Physics area (fills remaining space below controls) ------------------- + lv_obj_t* physArea = mcContainer(root); + lv_obj_set_width(physArea, LV_PCT(100)); + lv_obj_set_flex_grow(physArea, 1); + lv_obj_set_style_bg_color(physArea, lv_color_hex(0x080C12), 0); + lv_obj_set_style_bg_opa(physArea, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(physArea, 0, 0); + lv_obj_set_style_pad_all(physArea, 0, 0); + lv_obj_set_scrollable(physArea, false); + lv_obj_add_event_cb(physArea, cbZenDraw, LV_EVENT_DRAW_MAIN, mc); + lv_obj_add_event_cb(physArea, cbCanvasSize, LV_EVENT_SIZE_CHANGED, mc); + zn.canvas = physArea; + + // Start timer + mc->zenTimer_ = lv_timer_create(cbZenTimer, 16, mc); +} diff --git a/Apps/MidiController/main/Source/main.cpp b/Apps/MidiController/main/Source/main.cpp new file mode 100644 index 0000000..3e86b32 --- /dev/null +++ b/Apps/MidiController/main/Source/main.cpp @@ -0,0 +1,64 @@ +#include "MidiController.h" + +#include +#include +#include + +#include + +#include + +#include + +extern "C" { + +int main(int argc, char* argv[]) { + AppInstanceId app_instance_id = app_scheduler_current_app_id(); + + // Heap-allocated: the BT event subscription (bluetooth_event_subscribe, see + // MidiController::createWidgetsImpl) captures the instance's address, so it can't be a stack + // frame that goes away while a subscription might still be active. + auto mc = std::make_unique(); + mc->appInstanceId = app_instance_id; + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + struct AppEventSubscription sub {}; + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + + // BLE and USB MIDI start on this task: the LVGL task can't do the flash and NVS work that they need + mc->attachEventGroup(&event_group); + // Before detectBleState(): a BLE session that's already running must override the saved choice. + mc->loadSettings(); + mc->detectBleState(); + + WindowId window = window_manager_create(app_instance_id, MidiController::createWidgets, mc.get()); + mc->window = window; + + bool should_close = false; + while (!should_close) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + struct AppEvent event; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + should_close = true; + break; + } + } + + if (!should_close) { + mc->processEvents(); + } + } + + window_manager_remove(window); + app_event_unsubscribe(&sub); + mc->teardown(); + task_event_group_destruct(&event_group); + + return 0; +} + +} diff --git a/Apps/MidiController/manifest.properties b/Apps/MidiController/manifest.properties new file mode 100644 index 0000000..8d202ed --- /dev/null +++ b/Apps/MidiController/manifest.properties @@ -0,0 +1,12 @@ +manifest.version=0.3 +target.sdk=0.8.0-dev +target.platforms=esp32p4 +id=tactility.midicontroller +version.name=0.5.0 +version.code=5 +name=Midi Controller +description=Midi Controller +app.0.id=tactility.midicontroller +app.0.name=Midi Controller +app.0.binary=midicontroller +requires.device.id=m5stack-tab5 diff --git a/Apps/MystifyDemo/main/CMakeLists.txt b/Apps/MystifyDemo/main/CMakeLists.txt index 6121d68..d64a8cc 100644 --- a/Apps/MystifyDemo/main/CMakeLists.txt +++ b/Apps/MystifyDemo/main/CMakeLists.txt @@ -1,4 +1,11 @@ include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") file(GLOB_RECURSE SOURCE_FILES Source/*.c*) -tactility_component_register(SRCS ${SOURCE_FILES} INCLUDE_DIRS "Include") +file(GLOB_RECURSE GFX_FILES ../../../Libraries/Gfx/Source/*.c*) +file(GLOB_RECURSE INPUT_FILES ../../../Libraries/Input/Source/*.c*) + +tactility_component_register( + SRCS ${SOURCE_FILES} ${GFX_FILES} ${INPUT_FILES} + INCLUDE_DIRS "Include" ../../../Libraries/Gfx/Include ../../../Libraries/Input/Include + REQUIRES graphics-module esp_driver_ppa +) diff --git a/Apps/MystifyDemo/main/Include/Application.h b/Apps/MystifyDemo/main/Include/Application.h index 90bcf71..a84e34c 100644 --- a/Apps/MystifyDemo/main/Include/Application.h +++ b/Apps/MystifyDemo/main/Include/Application.h @@ -1,6 +1,6 @@ #pragma once -#include "drivers/DisplayDriver.h" -#include "drivers/TouchDriver.h" +#include +#include -void runApplication(DisplayDriver* display, TouchDriver* touch); +void runApplication(gfx::Target* target, AnyInput* input); diff --git a/Apps/MystifyDemo/main/Include/MystifyDemo.h b/Apps/MystifyDemo/main/Include/MystifyDemo.h index 9be2c48..1293cd9 100644 --- a/Apps/MystifyDemo/main/Include/MystifyDemo.h +++ b/Apps/MystifyDemo/main/Include/MystifyDemo.h @@ -1,97 +1,77 @@ #pragma once -#include "PixelBuffer.h" -#include "drivers/DisplayDriver.h" +#include + #include #include + #include /** - * Mystify Screensaver Demo - * - * Classic Windows-style mystify screensaver with bouncing polygons and trailing edges. - * Adapted to work with DisplayDriver and PixelBuffer abstractions. + * Mystify screensaver demo. * - * Usage: - * MystifyDemo mystify; - * mystify.init(display); + * Draws two bouncing polygons with trailing edges. Rendering goes through gfx::Target, which picks + * the cheapest way onto the panel (frame buffer flip, dirty regions, or bands), so the same drawing + * code runs on MIPI, RGB, SPI and e-paper panels regardless of how much RAM they have. * - * while (!shouldExit) { - * mystify.update(); - * } + * The trails are erased by redrawing the snapshots that dropped out of them in black, instead of + * clearing each polygon's bounding box. The box is large (four independently bouncing vertices span + * much of the screen) while the dropped snapshots are few, and on a hardware frame buffer every + * touched cache line costs a fill and a writeback. Erasing the ones from the previous two frames + * also repairs the back buffer, which still holds the frame before last, so the target doesn't have + * to copy anything forward - hence Prepare::Overwrite below. */ class MystifyDemo { public: static constexpr int NUM_POLYGONS = 2; static constexpr int NUM_VERTICES = 4; static constexpr int TRAIL_LENGTH = 8; - static constexpr int COLOR_CHANGE_INTERVAL = 200; // Frames between color changes - static constexpr int STRIP_HEIGHT = 16; // Draw in strips to avoid SPI buffer overflow + static constexpr int COLOR_CHANGE_INTERVAL = 200; // Frames between colour changes + // Two, because a double-buffered target displays the frame before last when this one is drawn + static constexpr int ERASE_AGES = 2; + // One snapshot per trail entry, plus enough to still reproduce the trails of ERASE_AGES frames + // ago: a ring of only TRAIL_LENGTH slots loses the oldest snapshot the moment it advances. + static constexpr int HISTORY_LENGTH = TRAIL_LENGTH + ERASE_AGES; MystifyDemo() = default; - ~MystifyDemo() { deinit(); } + ~MystifyDemo() = default; - // Non-copyable, non-movable (owns PixelBuffer) MystifyDemo(const MystifyDemo&) = delete; MystifyDemo& operator=(const MystifyDemo&) = delete; MystifyDemo(MystifyDemo&&) = delete; MystifyDemo& operator=(MystifyDemo&&) = delete; - bool init(DisplayDriver* display) { - if (!display) { + /** @param target must outlive this object */ + bool init(gfx::Target* target) { + if (target == nullptr || !target->valid()) { return false; } - display_ = display; - width_ = display->getWidth(); - height_ = display->getHeight(); - + target_ = target; + width_ = target->info().width; + height_ = target->info().height; if (width_ <= 0 || height_ <= 0) { return false; } - // Seed random generator with hardware entropy srand(static_cast(esp_random())); - // Allocate full-screen framebuffer - void* mem = malloc(sizeof(PixelBuffer)); - if (!mem) { - return false; - } - framebuffer_ = new(mem) PixelBuffer(width_, height_, display->getColorFormat()); - initPolygons(); return true; } - void deinit() { - if (framebuffer_) { - framebuffer_->~PixelBuffer(); - free(framebuffer_); - framebuffer_ = nullptr; - } - display_ = nullptr; - } - void update() { - if (!framebuffer_ || !display_) return; - - // Clear framebuffer to black - framebuffer_->clear(); - - // Update and draw each polygon - for (int p = 0; p < NUM_POLYGONS; p++) { - updatePolygon(polygons_[p]); - drawPolygon(polygons_[p]); + if (target_ == nullptr) { + return; } - // Send framebuffer to display in strips (full screen is too large for single SPI transaction) - display_->lock(); - for (int y = 0; y < height_; y += STRIP_HEIGHT) { - int stripEnd = (y + STRIP_HEIGHT > height_) ? height_ : y + STRIP_HEIGHT; - display_->drawBitmap(0, y, width_, stripEnd, framebuffer_->getDataAtRow(y)); + for (auto& polygon : polygons_) { + advance(polygon); } - display_->unlock(); + + target_->render([this](gfx::Surface& surface) { + draw(surface); + }, gfx::Prepare::Overwrite); } private: @@ -105,34 +85,34 @@ class MystifyDemo { struct Polygon { Vertex vertices[NUM_VERTICES]; - // History: [trail_index][vertex_index] = {x, y} - int16_t historyX[TRAIL_LENGTH][NUM_VERTICES]; - int16_t historyY[TRAIL_LENGTH][NUM_VERTICES]; - uint8_t colorIndex; + // History: [snapshot_index][vertex_index] = {x, y} + int16_t historyX[HISTORY_LENGTH][NUM_VERTICES]; + int16_t historyY[HISTORY_LENGTH][NUM_VERTICES]; + uint8_t colorIndex = 0; int historyHead = 0; - bool historyFull = false; int colorChangeCounter = 0; }; - // Vibrant colors as RGB888 for format-agnostic rendering + // Vibrant colours as RGB888, converted per pixel for format-agnostic rendering struct Color { - uint8_t r, g, b; + uint8_t r; + uint8_t g; + uint8_t b; }; static constexpr Color COLOR_POOL[] = { - {255, 0, 255}, // Magenta - {0, 255, 255}, // Cyan - {255, 255, 0}, // Yellow - {255, 128, 0}, // Orange - {0, 255, 128}, // Spring green - {128, 0, 255}, // Purple - {255, 64, 128}, // Hot pink - {128, 255, 0}, // Lime + { 255, 0, 255 }, // Magenta + { 0, 255, 255 }, // Cyan + { 255, 255, 0 }, // Yellow + { 255, 128, 0 }, // Orange + { 0, 255, 128 }, // Spring green + { 128, 0, 255 }, // Purple + { 255, 64, 128 }, // Hot pink + { 128, 255, 0 }, // Lime }; static constexpr int COLOR_POOL_SIZE = sizeof(COLOR_POOL) / sizeof(COLOR_POOL[0]); - DisplayDriver* display_ = nullptr; - PixelBuffer* framebuffer_ = nullptr; + gfx::Target* target_ = nullptr; uint16_t width_ = 0; uint16_t height_ = 0; Polygon polygons_[NUM_POLYGONS]; @@ -141,24 +121,21 @@ class MystifyDemo { return min + (max - min) * (static_cast(rand()) / static_cast(RAND_MAX)); } - void initPolygons() { - for (int p = 0; p < NUM_POLYGONS; p++) { - Polygon& polygon = polygons_[p]; + static uint16_t toRgb565(uint8_t r, uint8_t g, uint8_t b) { + return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); + } - // Pick random color from pool - polygon.colorIndex = rand() % COLOR_POOL_SIZE; + void initPolygons() { + for (auto& polygon : polygons_) { + polygon.colorIndex = static_cast(rand() % COLOR_POOL_SIZE); polygon.historyHead = 0; - polygon.historyFull = false; - // Stagger color changes so polygons don't change simultaneously + // Stagger colour changes so both polygons don't change at the same time polygon.colorChangeCounter = rand() % COLOR_CHANGE_INTERVAL; - // Initialize vertices with random positions and velocities - for (int v = 0; v < NUM_VERTICES; v++) { - Vertex& vertex = polygon.vertices[v]; + for (auto& vertex : polygon.vertices) { vertex.x = static_cast(rand() % width_); vertex.y = static_cast(rand() % height_); - // Speed range for smooth movement vertex.dx = randomFloat(0.8f, 2.0f); vertex.dy = randomFloat(0.8f, 2.0f); if (rand() % 2) vertex.dx = -vertex.dx; @@ -170,8 +147,7 @@ class MystifyDemo { } } - // Initialize history with current positions - for (int t = 0; t < TRAIL_LENGTH; t++) { + for (int t = 0; t < HISTORY_LENGTH; t++) { for (int v = 0; v < NUM_VERTICES; v++) { polygon.historyX[t][v] = static_cast(polygon.vertices[v].x); polygon.historyY[t][v] = static_cast(polygon.vertices[v].y); @@ -180,29 +156,26 @@ class MystifyDemo { } } - void updatePolygon(Polygon& polygon) { + /** Moves the polygon and records its new position. Drawing is a separate step. */ + void advance(Polygon& polygon) { constexpr float minSpeed = 0.5f; constexpr float maxSpeed = 2.5f; - // Periodic color change polygon.colorChangeCounter++; if (polygon.colorChangeCounter >= COLOR_CHANGE_INTERVAL) { polygon.colorChangeCounter = 0; - // Pick a different color uint8_t newColor; do { - newColor = rand() % COLOR_POOL_SIZE; + newColor = static_cast(rand() % COLOR_POOL_SIZE); } while (newColor == polygon.colorIndex && COLOR_POOL_SIZE > 1); polygon.colorIndex = newColor; } - // Move vertices - for (int v = 0; v < NUM_VERTICES; v++) { - Vertex& vertex = polygon.vertices[v]; + for (auto& vertex : polygon.vertices) { vertex.x += vertex.dx; vertex.y += vertex.dy; - // Bounce off edges with slight angle variation for organic movement + // Bounce off the edges with a slight angle variation for organic movement if (vertex.x <= 0) { vertex.x = 0; vertex.dx = std::fabs(vertex.dx); @@ -223,95 +196,78 @@ class MystifyDemo { vertex.dx *= (1.0f + randomFloat(-0.1f, 0.1f)); } - // Clamp speeds to prevent runaway acceleration or stalling - auto clampSpeed = [minSpeed, maxSpeed](float& speed) { - float sign = (speed >= 0) ? 1.0f : -1.0f; - float absSpeed = std::fabs(speed); - if (absSpeed < minSpeed) absSpeed = minSpeed; - if (absSpeed > maxSpeed) absSpeed = maxSpeed; - speed = sign * absSpeed; + auto clampSpeed = [](float& speed) { + const float sign = (speed >= 0) ? 1.0f : -1.0f; + float absolute = std::fabs(speed); + if (absolute < minSpeed) absolute = minSpeed; + if (absolute > maxSpeed) absolute = maxSpeed; + speed = sign * absolute; }; clampSpeed(vertex.dx); clampSpeed(vertex.dy); } - // Advance history ring buffer - polygon.historyHead = (polygon.historyHead + 1) % TRAIL_LENGTH; - if (polygon.historyHead == 0) { - polygon.historyFull = true; - } + // Advance the history ring buffer + polygon.historyHead = (polygon.historyHead + 1) % HISTORY_LENGTH; - // Store current positions for (int v = 0; v < NUM_VERTICES; v++) { polygon.historyX[polygon.historyHead][v] = static_cast(polygon.vertices[v].x); polygon.historyY[polygon.historyHead][v] = static_cast(polygon.vertices[v].y); } } - void drawPolygon(const Polygon& polygon) { - const Color& baseColor = COLOR_POOL[polygon.colorIndex]; + /** Draws the polygon's single quad for one snapshot @a offset frames before the newest. */ + void drawSnapshot(gfx::Surface& surface, const Polygon& polygon, int offset, uint16_t color) const { + int index = polygon.historyHead - offset; + while (index < 0) { + index += HISTORY_LENGTH; + } - // Draw trail from oldest to newest (so newest is on top) - for (int t = TRAIL_LENGTH - 1; t >= 0; t--) { - int histIndex = polygon.historyHead - t; - if (histIndex < 0) histIndex += TRAIL_LENGTH; + for (int e = 0; e < NUM_VERTICES; e++) { + const int nextVertex = (e + 1) % NUM_VERTICES; + surface.line( + polygon.historyX[index][e], + polygon.historyY[index][e], + polygon.historyX[index][nextVertex], + polygon.historyY[index][nextVertex], + color + ); + } + } - // Skip if we don't have enough history yet - if (!polygon.historyFull && histIndex > polygon.historyHead) { - continue; - } + /** Draws the current trail, oldest snapshot first so the newest ends up on top. */ + void drawTrail(gfx::Surface& surface, const Polygon& polygon) const { + const Color& baseColor = COLOR_POOL[polygon.colorIndex]; - // Calculate brightness for this trail frame (older = dimmer) + for (int t = TRAIL_LENGTH - 1; t >= 0; t--) { + // Older trail frames are dimmer int brightness = 255 - (t * 230 / TRAIL_LENGTH); if (brightness < 25) brightness = 25; - // Scale color by brightness - uint8_t r = (baseColor.r * brightness) / 255; - uint8_t g = (baseColor.g * brightness) / 255; - uint8_t b = (baseColor.b * brightness) / 255; + const uint16_t color = toRgb565( + static_cast(baseColor.r * brightness / 255), + static_cast(baseColor.g * brightness / 255), + static_cast(baseColor.b * brightness / 255) + ); - // Draw edges connecting vertices - for (int e = 0; e < NUM_VERTICES; e++) { - int nextVertex = (e + 1) % NUM_VERTICES; - - int x0 = polygon.historyX[histIndex][e]; - int y0 = polygon.historyY[histIndex][e]; - int x1 = polygon.historyX[histIndex][nextVertex]; - int y1 = polygon.historyY[histIndex][nextVertex]; - - drawLine(x0, y0, x1, y1, r, g, b); - } + drawSnapshot(surface, polygon, t, color); } } - // Bresenham's line algorithm - void drawLine(int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t b) { - int dx = std::abs(x1 - x0); - int dy = std::abs(y1 - y0); - int sx = (x0 < x1) ? 1 : -1; - int sy = (y0 < y1) ? 1 : -1; - int err = dx - dy; - - while (true) { - setPixel(x0, y0, r, g, b); - - if (x0 == x1 && y0 == y1) break; - - int e2 = 2 * err; - if (e2 > -dy) { - err -= dy; - x0 += sx; - } - if (e2 < dx) { - err += dx; - y0 += sy; + /** Draws one frame. Safe to call more than once per update (banded mode does). */ + void draw(gfx::Surface& surface) const { + // Only the snapshots that dropped out of the trail since the frame in the back buffer was + // drawn need erasing: the ones still in the trail are redrawn anyway, with the brightness + // of their new position. Erasing one polygon after another has been drawn would wipe that + // one's trail, so every erase happens before any draw. + for (const auto& polygon : polygons_) { + for (int offset = TRAIL_LENGTH; offset < TRAIL_LENGTH + ERASE_AGES; offset++) { + drawSnapshot(surface, polygon, offset, 0x0000); } } - } - void setPixel(int x, int y, uint8_t r, uint8_t g, uint8_t b) { - if (x >= 0 && x < width_ && y >= 0 && y < height_) { - framebuffer_->setPixel(x, y, r, g, b); + for (const auto& polygon : polygons_) { + drawTrail(surface, polygon); } } }; diff --git a/Apps/MystifyDemo/main/Include/PixelBuffer.h b/Apps/MystifyDemo/main/Include/PixelBuffer.h deleted file mode 100644 index 1901927..0000000 --- a/Apps/MystifyDemo/main/Include/PixelBuffer.h +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once - -#include -#include "drivers/Colors.h" - -#include -#include - -class PixelBuffer { - uint16_t pixelWidth; - uint16_t pixelHeight; - enum DisplayColorFormat colorFormat; - uint8_t* data; - -public: - - PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, enum DisplayColorFormat colorFormat) : - pixelWidth(pixelWidth), - pixelHeight(pixelHeight), - colorFormat(colorFormat) - { - data = static_cast(malloc(pixelWidth * pixelHeight * getPixelSize())); - assert(data != nullptr); - } - - ~PixelBuffer() { - free(data); - } - - uint16_t getPixelWidth() const { - return pixelWidth; - } - - uint16_t getPixelHeight() const { - return pixelHeight; - } - - enum DisplayColorFormat getColorFormat() const { - return colorFormat; - } - - void* getData() const { - return data; - } - - uint32_t getDataSize() const { - return pixelWidth * pixelHeight * getPixelSize(); - } - - void* getDataAtRow(uint16_t row) const { - auto address = reinterpret_cast(data) + (row * getRowDataSize()); - return reinterpret_cast(address); - } - - uint16_t getRowDataSize() const { - return pixelWidth * getPixelSize(); - } - - uint8_t getPixelSize() const { - switch (colorFormat) { - case DISPLAY_COLOR_FORMAT_MONOCHROME: - return 1; - case DISPLAY_COLOR_FORMAT_BGR565: - case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: - case DISPLAY_COLOR_FORMAT_RGB565: - case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: - return 2; - case DISPLAY_COLOR_FORMAT_RGB888: - return 3; - default: - // TODO: Crash with error - return 0; - } - } - - uint8_t* getPixelAddress(uint16_t x, uint16_t y) const { - uint32_t offset = ((y * getPixelWidth()) + x) * getPixelSize(); - uint32_t address = reinterpret_cast(data) + offset; - return reinterpret_cast(address); - } - - void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const { - auto address = getPixelAddress(x, y); - switch (colorFormat) { - case DISPLAY_COLOR_FORMAT_MONOCHROME: - *address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3); - break; - case DISPLAY_COLOR_FORMAT_BGR565: - Colors::rgb888ToBgr565(r, g, b, reinterpret_cast(address)); - break; - case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: { - // TODO: Make proper conversion function - Colors::rgb888ToBgr565(r, g, b, reinterpret_cast(address)); - uint8_t temp = *address; - *address = *(address + 1); - *(address + 1) = temp; - break; - } - case DISPLAY_COLOR_FORMAT_RGB565: { - Colors::rgb888ToRgb565(r, g, b, reinterpret_cast(address)); - break; - } - case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: { - // TODO: Make proper conversion function - Colors::rgb888ToRgb565(r, g, b, reinterpret_cast(address)); - uint8_t temp = *address; - *address = *(address + 1); - *(address + 1) = temp; - break; - } - case DISPLAY_COLOR_FORMAT_RGB888: { - uint8_t pixel[3] = { r, g, b }; - memcpy(address, pixel, 3); - break; - } - default: - // NO-OP - break; - } - } - - void clear(int value = 0) const { - memset(data, value, getDataSize()); - } -}; \ No newline at end of file diff --git a/Apps/MystifyDemo/main/Include/drivers/Colors.h b/Apps/MystifyDemo/main/Include/drivers/Colors.h deleted file mode 100644 index d068574..0000000 --- a/Apps/MystifyDemo/main/Include/drivers/Colors.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -class Colors { - -public: - - static void rgb888ToRgb565(uint8_t red, uint8_t green, uint8_t blue, uint16_t* rgb565) { - uint16_t _rgb565 = (red >> 3); - _rgb565 = (_rgb565 << 6) | (green >> 2); - _rgb565 = (_rgb565 << 5) | (blue >> 3); - *rgb565 = _rgb565; - } - - static void rgb888ToBgr565(uint8_t red, uint8_t green, uint8_t blue, uint16_t* bgr565) { - uint16_t _bgr565 = (blue >> 3); - _bgr565 = (_bgr565 << 6) | (green >> 2); - _bgr565 = (_bgr565 << 5) | (red >> 3); - *bgr565 = _bgr565; - } - - static void rgb565ToRgb888(uint16_t rgb565, uint32_t* rgb888) { - uint32_t _rgb565 = rgb565; - uint8_t b = (_rgb565 >> 8) & 0xF8; - uint8_t g = (_rgb565 >> 3) & 0xFC; - uint8_t r = (_rgb565 << 3) & 0xF8; - - uint8_t* r8p = reinterpret_cast(rgb888); - uint8_t* g8p = r8p + 1; - uint8_t* b8p = r8p + 2; - - *r8p = r | ((r >> 3) & 0x7); - *g8p = g | ((g >> 2) & 0x3); - *b8p = b | ((b >> 3) & 0x7); - } -}; \ No newline at end of file diff --git a/Apps/MystifyDemo/main/Include/drivers/DisplayDriver.h b/Apps/MystifyDemo/main/Include/drivers/DisplayDriver.h deleted file mode 100644 index 5e3916a..0000000 --- a/Apps/MystifyDemo/main/Include/drivers/DisplayDriver.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include - -/** - * Wrapper for display_* device driver functions - */ -class DisplayDriver { - - struct Device* device; - -public: - - explicit DisplayDriver(struct Device* device) : device(device) { - device_get(device); - } - - ~DisplayDriver() { - device_put(device); - } - - bool lock(TickType_t timeout = tt::kernel::FREERTOS_MAX_TICKS) const { - return device_try_lock(device, timeout); - } - - void unlock() const { - device_unlock(device); - } - - uint16_t getWidth() const { - return display_get_resolution_x(device); - } - - uint16_t getHeight() const { - return display_get_resolution_y(device); - } - - enum DisplayColorFormat getColorFormat() const { - return display_get_color_format(device); - } - - void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const { - display_draw_bitmap(device, xStart, yStart, xEnd, yEnd, pixelData); - } -}; diff --git a/Apps/MystifyDemo/main/Include/drivers/TouchDriver.h b/Apps/MystifyDemo/main/Include/drivers/TouchDriver.h deleted file mode 100644 index 1b75369..0000000 --- a/Apps/MystifyDemo/main/Include/drivers/TouchDriver.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include - -/** - * Wrapper for touch_* and mouse_* device driver functions. - * Either device can be absent. A press of the primary mouse button counts as a touch. - */ -class TouchDriver { - - struct Device* touchDevice; - struct Device* mouseDevice; - -public: - - TouchDriver(struct Device* touchDevice, struct Device* mouseDevice) : touchDevice(touchDevice), mouseDevice(mouseDevice) { - if (touchDevice != nullptr) { - device_get(touchDevice); - } - if (mouseDevice != nullptr) { - device_get(mouseDevice); - } - } - - ~TouchDriver() { - if (touchDevice != nullptr) { - device_put(touchDevice); - } - if (mouseDevice != nullptr) { - device_put(mouseDevice); - } - } - - bool isTouched() const { - if (touchDevice != nullptr) { - uint16_t x, y, strength; - uint8_t count = 0; - // Poll without blocking: perform one read attempt, then report whatever is cached. - touch_read_data(touchDevice, 0); - if (touch_get_touched_points(touchDevice, &x, &y, &strength, &count, 1)) { - return true; - } - } - bool pressed = false; - return mouseDevice != nullptr && mouse_get_button_pressed(mouseDevice, 0, &pressed) == ERROR_NONE && pressed; - } -}; diff --git a/Apps/MystifyDemo/main/Source/Application.cpp b/Apps/MystifyDemo/main/Source/Application.cpp index 7a70909..269245d 100644 --- a/Apps/MystifyDemo/main/Source/Application.cpp +++ b/Apps/MystifyDemo/main/Source/Application.cpp @@ -1,34 +1,110 @@ #include "Application.h" #include "MystifyDemo.h" -#include "PixelBuffer.h" -#include "esp_log.h" + +#include +#include + +#include +#include #include constexpr auto TAG = "Application"; -constexpr int MYSTIFY_FRAME_DELAY_MS = 50; // ~20 FPS for smooth animation -static bool isTouched(TouchDriver* touch) { - return touch->isTouched(); -} +// The frame budget is measured from the start of a frame, so drawing time counts towards it +// instead of being added to it. It's only a cap: a slower panel just runs slower. +constexpr int TARGET_FPS = 60; + +// Prints fps and per-phase render timings once a second. Useful when bringing up a new panel. +constexpr bool LOG_STATS = false; -void runApplication(DisplayDriver* display, TouchDriver* touch) { - // Run the Mystify screensaver demo +void runApplication(gfx::Target* target, AnyInput* input) { MystifyDemo mystify; - if (!mystify.init(display)) { + if (!mystify.init(target)) { ESP_LOGE(TAG, "Failed to initialize MystifyDemo"); return; } - ESP_LOGI(TAG, "Starting Mystify demo - touch to exit"); + const gfx::Info& info = target->info(); + ESP_LOGI(TAG, "Starting Mystify demo (%dx%d) - any input to exit", info.width, info.height); + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + struct AppEventSubscription sub {}; + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + + const int64_t frameBudgetUs = 1000000 / TARGET_FPS; + + bool should_close = false; + + int64_t window_start_us = esp_timer_get_time(); + int window_frames = 0; + int64_t sum_copy_us = 0; + int64_t sum_draw_us = 0; + int64_t sum_sync_us = 0; + int64_t sum_present_us = 0; + int64_t sum_total_us = 0; do { + const int64_t frame_start_us = esp_timer_get_time(); + mystify.update(); - // Frame rate limiter - ~20 FPS for smooth animation - tt::kernel::delayTicks(tt::kernel::millisToTicks(MYSTIFY_FRAME_DELAY_MS)); - } while (!isTouched(touch)); + if (LOG_STATS) { + const gfx::Stats& stats = target->stats(); + sum_copy_us += stats.copyUs; + sum_draw_us += stats.drawUs; + sum_sync_us += stats.syncUs; + sum_present_us += stats.presentUs; + sum_total_us += stats.totalUs; + } + + struct AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + should_close = true; + } + } + if (should_close) { + break; + } + + const int64_t frame_us = esp_timer_get_time() - frame_start_us; + if (frame_us < frameBudgetUs) { + const int64_t remaining_ms = (frameBudgetUs - frame_us + 999) / 1000; + tt::kernel::delayTicks(tt::kernel::millisToTicks(static_cast(remaining_ms))); + } + + if (LOG_STATS) { + window_frames++; + const int64_t elapsed_us = esp_timer_get_time() - window_start_us; + if (elapsed_us >= 1000000) { + const gfx::Stats& stats = target->stats(); + const int fps = static_cast((static_cast(window_frames) * 1000000) / elapsed_us); + const int frames = window_frames; + ESP_LOGI(TAG, "%d fps, %d rects, %d px | copy %d draw %d sync %d present %d us (total %d)", + fps, + stats.dirtyRects, + stats.dirtyPixels, + static_cast(sum_copy_us / frames), + static_cast(sum_draw_us / frames), + static_cast(sum_sync_us / frames), + static_cast(sum_present_us / frames), + static_cast(sum_total_us / frames) + ); + window_start_us = esp_timer_get_time(); + window_frames = 0; + sum_copy_us = 0; + sum_draw_us = 0; + sum_sync_us = 0; + sum_present_us = 0; + sum_total_us = 0; + } + } + } while (!input->activity()); + + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); ESP_LOGI(TAG, "Mystify demo ended"); } - diff --git a/Apps/MystifyDemo/main/Source/Main.cpp b/Apps/MystifyDemo/main/Source/Main.cpp index cdab33d..9aca1b2 100644 --- a/Apps/MystifyDemo/main/Source/Main.cpp +++ b/Apps/MystifyDemo/main/Source/Main.cpp @@ -1,18 +1,18 @@ #include "Application.h" -#include "drivers/DisplayDriver.h" -#include "drivers/TouchDriver.h" #include #include +#include #include #include +#include +#include + #include #include #include -#include -#include #include #include #include @@ -25,7 +25,7 @@ constexpr auto TAG = "Main"; static void showErrorAndWait(AppInstanceId appInstanceId, const char* message) { const char* argv[] = { "Error", message, "OK" }; uint32_t dialogInstanceId = 0; - app_start_for_result("AlertDialog", 3, argv, appInstanceId, &dialogInstanceId); + app_start_for_result("tactility.alertdialog", 3, argv, appInstanceId, &dialogInstanceId); if (dialogInstanceId == 0) { return; } @@ -71,42 +71,48 @@ int main(int argc, char* argv[]) { return 0; } - struct Device* touch_device = nullptr; - struct Device* mouse_device = nullptr; - device_get_first_active_by_type(&TOUCH_TYPE, &touch_device); - device_get_first_active_by_type(&MOUSE_TYPE, &mouse_device); - if (touch_device == nullptr && mouse_device == nullptr) { - ESP_LOGE(TAG, "No touch device found"); - device_put(display_device); - showErrorAndWait(app_instance_id, "No touch device was found."); - return 0; - } - // Stop LVGL first (because it's currently using the drivers we want to use) module_stop(&lvgl_module); - ESP_LOGI(TAG, "Creating display driver"); - auto display = new DisplayDriver(display_device); + ESP_LOGI(TAG, "Creating display target"); + auto target = new gfx::Target(display_device); device_put(display_device); - ESP_LOGI(TAG, "Creating touch driver"); - auto touch = new TouchDriver(touch_device, mouse_device); - if (touch_device != nullptr) { - device_put(touch_device); + if (!target->valid()) { + ESP_LOGE(TAG, "Display doesn't support direct rendering"); + delete target; + + if (!module_is_started(&lvgl_module)) { + module_start(&lvgl_module); + } + + showErrorAndWait(app_instance_id, "This display can't be used for direct rendering."); + return 0; } - if (mouse_device != nullptr) { - device_put(mouse_device); + + auto input = new AnyInput(); + if (!input->available()) { + ESP_LOGE(TAG, "No input device found"); + delete input; + delete target; + + if (!module_is_started(&lvgl_module)) { + module_start(&lvgl_module); + } + + showErrorAndWait(app_instance_id, "No input device was found."); + return 0; } // Run the main logic ESP_LOGI(TAG, "Running application"); - runApplication(display, touch); + runApplication(target, input); - ESP_LOGI(TAG, "Cleanup display driver"); - delete display; + ESP_LOGI(TAG, "Cleanup display target"); + delete target; - ESP_LOGI(TAG, "Cleanup touch driver"); - delete touch; + ESP_LOGI(TAG, "Cleanup input"); + delete input; // Restart LVGL to resume rendering of regular apps if (!module_is_started(&lvgl_module)) { diff --git a/Apps/MystifySpiDemo/README.md b/Apps/MystifySpiDemo/README.md new file mode 100644 index 0000000..1643773 --- /dev/null +++ b/Apps/MystifySpiDemo/README.md @@ -0,0 +1,170 @@ +# Mystify SPI Demo + +An app package that adds a 2.8" ILI9341 SPI panel to Tactility and renders a Mystify scene on it +(and mirrors it on the primary display). + +The package contains five binaries: + +| Binary | Role | +|--------|------| +| `ili9341-panel-module` | Kernel module: owns the driver, the SPI bus and the display device, and exports a settings API | +| `mystifyspidemo` | The demo app: looks the panel up through the module's API and draws to it with Gfx | +| `ili9341-panel-settings` | The settings app ("ILI9341 Panel"): edits every panel setting and applies it live | +| `paneldashboard` | Panel Dashboard: draws a live clock and system stats on the panel's secondary `lv_display_t` | +| `ili9341panel` | Headless CLI, exposed by the shell as the `ili9341panel` command | + +Because the module is loaded from the package (at install and at boot), the panel exists as a +normal `DisplayApi` device even when no app is running. LVGL binds it as a secondary display when +it starts; opening the demo stops LVGL, takes over the panel through `gfx::SinkTarget` + +`gfx::DisplaySink`, and restarts LVGL on exit. + +## Wiring + +| Panel | Tab5 M-BUS | Note | +|-------|------------|------| +| VCC | Ext5V | The module has its own 3.3 V regulator | +| GND | GND | | +| CS | GPIO51 | | +| RESET | GPIO4 | | +| DC | GPIO47 | | +| MOSI | GPIO18 | | +| SCK | GPIO5 | | +| MISO | GPIO19 | Only needed for register reads | +| LED | 3V3 | Do **not** drive it from a GPIO: that stops the board from booting | + +## Module + +`ili9341-panel-module` starts at boot: + +1. Registers the `ili9341-spi` driver (`driver_construct_add`). +2. Loads `ili9341panel.properties` from the package's user data directory (defaults on first run). +3. Calls `spi_bus_initialize()` for the configured host/pins and creates the display device. +4. LVGL picks up the device when it starts and binds it as a second `lv_display_t`. + +On stop it removes the device and frees the bus. If the panel fails to start, the module still +stays loaded so the configuration can be fixed and re-applied. + +The driver itself (`Ili9341Spi.h`/`Ili9341Spi.cpp`, including the DisplayApi and MADCTL handling) +lives in the module too, next to `Panel.cpp`/`Settings.cpp`. It used to be the standalone +`Libraries/Ili9341Spi`; since the module is the only consumer and app-packaged drivers are proven, +keeping it there makes the package self-contained. + +### Settings API + +Exported by the module and resolved by apps at runtime (`module_resolve_symbol_global_get`): + +```c +error_t ili9341panel_settings_defaults(struct Ili9341PanelConfig* out_config); +error_t ili9341panel_settings_load(struct Ili9341PanelConfig* out_config); +error_t ili9341panel_settings_save(const struct Ili9341PanelConfig* config); +error_t ili9341panel_apply(void); // re-create the device from saved settings +bool ili9341panel_is_active(void); +error_t ili9341panel_get_device(struct Device** out_device); // caller must device_put() +``` + +Defaults match the wiring above: SPI2, SCK=5, MOSI=18, MISO=19, DC=47, RESET=4, CS=51, landscape +320x240, 40 MHz, BGR, no inversion. `ili9341panel_apply()` destroys and re-creates the device, so +the panel's `lv_display_t` must be removed first (see "Settings app" below). + +## Settings app + +`ili9341-panel-settings` is a window-manager app that edits the module's saved configuration: + +- **Panel**: enabled switch (starts/stops the module, so it matches Kernel settings), four + orientation presets, mirror X/Y, invert colours, BGR order. The portraits use mirror Y for this + panel, and the presets also set the matching 240×320/320×240 size. +- **Size and bus**: 320x240/240x320 presets with an active highlight plus free width/height, + SPI2/SPI3, 10/20/40/80 MHz, and the maximum transfer size. +- **Pins**: SCK, MOSI, MISO, DC, RESET (None = unused) and CS. +- **Save** stores the settings for the next boot; **Apply** saves them, restarts the device and + rebinds LVGL; **Revert** reloads the saved values into the form. + +Numeric values (width, height, max transfer, pins) are mesh-settings style rows: the row shows the +current value and its change button opens the Input Dialog app for a proper, full-size text field +(leave it empty for RESET = None). Nothing uses a dropdown or an inline textarea. + +Apply runs on the app task, not in the LVGL event callback. It finds the panel's `lv_display_t` via +`LvglDeviceContext.device`, removes it under the LVGL lock, calls `ili9341panel_apply()`, and then +re-adds it with the same `LvglDisplayConfig` that `lvgl_devices_attach()` uses. A configuration that +fails to start is rolled back to the previous one, so the panel doesn't stay dead after a bad pin +value. While the switch is off, Save/Apply/Revert only report that the panel must be enabled first. +This requires the +firmware exports `lv_display_get_next` and `lv_display_get_driver_data` (see "Build and install"). + +The enabled switch controls the panel module's started state through `app_module_set_started()`, so +Kernel settings shows the same Started/Stopped state. While the module is stopped the form shows the +built-in defaults; turning it on loads the saved settings. If the module is started from Kernel +settings while this app is in the background, the form re-syncs when the window is rebuilt. + +## Secondary display + +The panel is a real secondary `lv_display_t` while LVGL runs, not a copy of the primary screen. +Apps can put widgets on its screen with `lv_display_get_screen_active()` (exported by the lvgl +module) and find the display through `LvglDeviceContext`, which is what `Panel Dashboard` does: it +shows a live clock, date, uptime, internal RAM and PSRAM on the panel while its window (status and +help) stays on the primary display. Closing the app deletes its widgets and the panel goes blank. + +This model was chosen over mirroring the UI: the primary renders a 640x360 PPA-scaled frame, and +copying and scaling it to 320x240 on every refresh would cost more than the SPI panel can keep up +with. Apps that want raw pixels instead of LVGL widgets can still take the panel over with +`gfx::DisplaySink`, as the Mystify demo does. + +## Terminal + +`ili9341panel` is a headless app, so the shell exposes it as a command. It talks to the same +module API as the settings app: + +```sh +ili9341panel status # module/panel state, SPI and display settings +ili9341panel get # all settings as key=value +ili9341panel on | off # start/stop the module (same as the settings switch) +ili9341panel set orientation portrait +ili9341panel set clock 80 +ili9341panel set sck 5 # keys: sck mosi miso dc cs reset; reset also accepts none +ili9341panel apply # re-create the panel from the saved settings +``` + +`set` saves immediately and reports that `apply` is needed; `apply` detaches the panel's LVGL +display, calls `ili9341panel_apply()` and re-binds it. + +## Demo app + +- Resolves `ili9341panel_get_device` to hold a module reference and get the panel device. +- Stops LVGL (done by the entry point), then creates `ExternalFirstDisplaySink` + + `gfx::SinkTarget` at the device's own resolution. The sink subclass prefers PSRAM because + `gfx::DisplaySink`'s default policy requires internal DMA memory; the driver sets + `SPI_TRANS_DMA_USE_PSRAM` so the SPI engine reads directly from PSRAM. +- Mirrors the same normalised simulation on the primary display via `gfx::Target`, using + `set_transform()` on portrait-native panels (Tab5) with a 640x360 logical frame that the PPA + scales/rotates up. +- Clears both panels and restarts LVGL on exit. + +## Build and install + +```sh +tt app build --path Apps/MystifySpiDemo --architecture esp32p4 +tt app bir --path Apps/MystifySpiDemo --architecture esp32p4 --host +``` + +The package installs all five binaries; the module starts when the package is installed and again +on every boot. After a kernel-module update, reboot once if the module was already running. + +The settings app's live apply and the dashboard rely on exports in `Modules/lvgl-module/source/symbols.c` +(`lv_display_get_next`, `lv_display_get_driver_data`, `lv_display_get_screen_active`). A firmware +built before those were added makes those apps fail to load; rebuild and flash it, then re-run +`Scripts/check-app-symbols.py` against all five ELFs. + +## Troubleshooting + +- **`The ILI9341 panel module is not available`** — the package's module isn't loaded. Reboot, or + check the module state in Kernel settings. +- **`The ILI9341 panel is not active`** — the module is loaded but the device failed to start. The + module logs the driver's reason; check the wiring, then save/apply settings. +- **White screen with correct wiring** — toggle `invert_color` (`true` gives a negative image on + this module), or lower `pixel_clock_hz` to 10-20 MHz. +- **App fails to load with a missing `spi_*` or `lv_display_get_next`/`lv_display_get_driver_data` + symbol** — the firmware predates those exports in `Platforms/platform-esp32/source/module.cpp` + and `Modules/lvgl-module/source/symbols.c`; rebuild and flash it, then re-run + `Scripts/check-app-symbols.py`. +- The unit's controller doesn't implement RDDID (it returns zeros) but does answer register reads; + the driver logs `Panel responds; RDDID is not implemented by this controller` and continues. diff --git a/Apps/MystifySpiDemo/common/ili9341panel.h b/Apps/MystifySpiDemo/common/ili9341panel.h new file mode 100644 index 0000000..5640045 --- /dev/null +++ b/Apps/MystifySpiDemo/common/ili9341panel.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Configuration for the app-owned ILI9341 SPI panel. + * + * The defaults match the M5Stack Tab5 M-BUS wiring used by this package: + * SCK=5, MOSI=18, MISO=19, DC=47, RESET=4, CS=51, landscape 320x240. + */ +struct Ili9341PanelConfig { + /** Create the device on module start. The settings are still saved while false. */ + bool enabled; + + /** ESP-IDF SPI host (SPI2_HOST is 1). */ + int32_t spi_host; + int32_t pin_sck; + int32_t pin_mosi; + int32_t pin_miso; + int32_t pin_dc; + int32_t pin_reset; + int32_t pin_cs; + + /** Logical resolution after MADCTL is applied. */ + uint16_t width; + uint16_t height; + bool swap_xy; + bool mirror_x; + bool mirror_y; + bool invert_color; + bool bgr_order; + + uint32_t pixel_clock_hz; + uint32_t max_transfer_bytes; +}; + +/** Fills @a out_config with the built-in defaults. */ +error_t ili9341panel_settings_defaults(struct Ili9341PanelConfig* out_config); + +/** Loads the saved settings, or the defaults when nothing was saved yet. */ +error_t ili9341panel_settings_load(struct Ili9341PanelConfig* out_config); + +/** Persists the settings. They take effect on the next ili9341panel_apply() or module start. */ +error_t ili9341panel_settings_save(const struct Ili9341PanelConfig* config); + +/** + * Stops the panel (if active) and re-creates it from the saved settings. + * + * LVGL must not be bound to the display while this runs: remove the panel's lv_display_t first + * (the settings app does), or stop LVGL entirely (as the demo app does). + */ +error_t ili9341panel_apply(void); + +/** @return true while the display device exists and is started. */ +bool ili9341panel_is_active(void); + +/** + * Returns a new reference to the active display device. + * The caller must call device_put() when done. + * @retval ERROR_NOT_FOUND the panel is not active + */ +error_t ili9341panel_get_device(struct Device** out_device); + +#ifdef __cplusplus +} +#endif diff --git a/Apps/MystifySpiDemo/common/panel_display.h b/Apps/MystifySpiDemo/common/panel_display.h new file mode 100644 index 0000000..af7cba8 --- /dev/null +++ b/Apps/MystifySpiDemo/common/panel_display.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +/** + * Shared LVGL binding helpers for the ILI9341 panel. Used by the settings app, the dashboard and + * the CLI so they all bind the device exactly like the lvgl module's boot-time scan does. + */ + +#include +#include +#include + +#include +#include +#include + +namespace ili9341panelui { + +/** @return the lv_display bound to @a device, or nullptr. */ +inline lv_display_t* findLvglDisplay(struct Device* device) { + for (lv_display_t* display = lv_display_get_next(nullptr); display != nullptr; display = lv_display_get_next(display)) { + auto* context = static_cast(lv_display_get_driver_data(display)); + if (context != nullptr && context->device == device) { + return display; + } + } + return nullptr; +} + +/** Removes every lv_display bound to @a device. Caller must hold the LVGL lock. */ +inline void removeLvglDisplays(struct Device* device) { + lv_display_t* display = lv_display_get_next(nullptr); + while (display != nullptr) { + lv_display_t* next = lv_display_get_next(display); + auto* context = static_cast(lv_display_get_driver_data(display)); + if (context != nullptr && context->device == device) { + lvgl_display_remove(display); + } + display = next; + } +} + +/** + * Detaches the panel from LVGL so its device can be stopped or drawn to directly. + * @param[in] clear fills the panel before detaching, so it doesn't keep a stale frame + */ +inline void detachFromLvgl(struct Device* device, bool clear = false) { + if (clear) { + display_clear(device); + } + lvgl_lock(); + removeLvglDisplays(device); + lvgl_unlock(); +} + +/** Binds @a device as an lv_display with the same configuration the lvgl module uses at boot. */ +inline error_t attachToLvgl(struct Device* device) { + const uint16_t vertical_resolution = display_get_resolution_y(device); + const enum DisplayColorFormat format = display_get_color_format(device); + const bool can_hw_rotate = + display_has_capability(device, DISPLAY_CAPABILITY_CAP_SWAP_XY) && + display_has_capability(device, DISPLAY_CAPABILITY_CAP_MIRROR); + + const LvglDisplayConfig config { + .buffer_height = static_cast(vertical_resolution > 10 ? vertical_resolution / 10 : vertical_resolution), + .double_buffer = false, + .sw_rotate = !can_hw_rotate, + .swap_bytes = format == DISPLAY_COLOR_FORMAT_RGB565_SWAPPED || + format == DISPLAY_COLOR_FORMAT_BGR565_SWAPPED || + format == DISPLAY_COLOR_FORMAT_BGR565, + .force_full_frame = display_has_capability(device, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME), + .prefer_external_ram = display_has_capability(device, DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM), + }; + + lvgl_lock(); + // A device that was stopped and re-created can leave an older display pointing at it. + removeLvglDisplays(device); + lv_display_t* display = nullptr; + const error_t result = lvgl_display_add(device, &config, &display); + lvgl_unlock(); + return result; +} + +} // namespace ili9341panelui diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/CMakeLists.txt b/Apps/MystifySpiDemo/ili9341-panel-module/CMakeLists.txt new file mode 100644 index 0000000..c625be4 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(tactility.mystifyspidemo.ili9341-panel-module) +project(tactility.mystifyspidemo.ili9341-panel-module) +tactility_project_post(tactility.mystifyspidemo.ili9341-panel-module) diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/CMakeLists.txt b/Apps/MystifySpiDemo/ili9341-panel-module/main/CMakeLists.txt new file mode 100644 index 0000000..cd69c4f --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/CMakeLists.txt @@ -0,0 +1,12 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +tactility_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS + ../../common + # Only for the ESP-IDF headers: at load time the module resolves the SPI/GPIO entry points + # against the firmware's export table. + REQUIRES esp_driver_spi esp_driver_gpio log +) diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.cpp b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.cpp new file mode 100644 index 0000000..2885ef9 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.cpp @@ -0,0 +1,667 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "Ili9341Spi.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +constexpr auto* TAG = "Ili9341Spi"; + +namespace { + +// MIPI DCS commands used here (ILI9341 datasheet section 8). Kept local so the driver does not +// need ESP-IDF's esp_lcd headers, which are a firmware-internal dependency. +constexpr uint8_t CMD_SWRESET = 0x01; +constexpr uint8_t CMD_RDDID = 0x04; +constexpr uint8_t CMD_RDD_MADCTL = 0x0B; +constexpr uint8_t CMD_SLPIN = 0x10; +constexpr uint8_t CMD_SLPOUT = 0x11; +constexpr uint8_t CMD_INVOFF = 0x20; +constexpr uint8_t CMD_INVON = 0x21; +constexpr uint8_t CMD_DISPOFF = 0x28; +constexpr uint8_t CMD_DISPON = 0x29; +constexpr uint8_t CMD_CASET = 0x2A; +constexpr uint8_t CMD_RASET = 0x2B; +constexpr uint8_t CMD_RAMWR = 0x2C; +constexpr uint8_t CMD_MADCTL = 0x36; +constexpr uint8_t CMD_COLMOD = 0x3A; + +// MADCTL bits +constexpr uint8_t MADCTL_MY = 1 << 7; +constexpr uint8_t MADCTL_MX = 1 << 6; +constexpr uint8_t MADCTL_MV = 1 << 5; +constexpr uint8_t MADCTL_BGR = 1 << 3; + +/** One entry of the vendor init table: command, parameters, and a post-command delay. */ +struct InitCommand { + uint8_t command; + uint8_t data[15]; + uint8_t data_size; + uint16_t delay_ms; +}; + +// Init sequence matches esp_lcd_ili9341's default (proven on real ILI9341 panels), minus the +// duplicate pump-ratio entry it carries. +constexpr InitCommand INIT_COMMANDS[] = { + { 0xCF, { 0x00, 0xAA, 0xE0 }, 3, 0 }, // Power control B + { 0xED, { 0x67, 0x03, 0x12, 0x81 }, 4, 0 }, // Power on sequence + { 0xE8, { 0x8A, 0x01, 0x78 }, 3, 0 }, // Driver timing A + { 0xCB, { 0x39, 0x2C, 0x00, 0x34, 0x02 }, 5, 0 }, // Power control A + { 0xF7, { 0x20 }, 1, 0 }, // Pump ratio + { 0xEA, { 0x00, 0x00 }, 2, 0 }, // Driver timing B + { 0xC0, { 0x23 }, 1, 0 }, // Power control 1 + { 0xC1, { 0x11 }, 1, 0 }, // Power control 2 + { 0xC5, { 0x43, 0x4C }, 2, 0 }, // VCOM control 1 + { 0xC7, { 0xA0 }, 1, 0 }, // VCOM control 2 + { 0xB1, { 0x00, 0x1B }, 2, 0 }, // Frame rate + { 0xF2, { 0x00 }, 1, 0 }, // 3-gamma off + { 0x26, { 0x01 }, 1, 0 }, // Gamma curve 1 + { 0xE0, { 0x1F, 0x36, 0x36, 0x3A, 0x0C, 0x05, 0x4F, 0x87, 0x3C, + 0x08, 0x11, 0x35, 0x19, 0x13, 0x00 }, 15, 0 }, // Positive gamma + { 0xE1, { 0x00, 0x09, 0x09, 0x05, 0x13, 0x0A, 0x30, 0x78, 0x43, + 0x07, 0x0E, 0x0A, 0x26, 0x2C, 0x1F }, 15, 0 }, // Negative gamma + { 0xB7, { 0x07 }, 1, 0 }, // Entry mode + { 0xB6, { 0x08, 0x82, 0x27 }, 3, 0 }, // Display function +}; + +struct State { + spi_device_handle_t device = nullptr; + int pin_dc = -1; + int pin_cs = -1; + uint8_t madctl = 0; + int32_t gap_x = 0; + int32_t gap_y = 0; + size_t max_transfer_bytes = 4096; +}; + +State* getState(Device* device) { + return static_cast(device_get_driver_data(device)); +} + +const Ili9341SpiConfig* getConfig(Device* device) { + return static_cast(device->config); +} + +/** + * One synchronous SPI transaction. `keep_cs` leaves chip select asserted so the next transaction + * continues the same panel command (command byte -> parameters, or RAMWR -> pixel data). + */ +error_t transmit(State* state, const void* data, size_t size, bool keep_cs, bool dc_high) { + if (state->device == nullptr || data == nullptr || size == 0 || size > state->max_transfer_bytes) { + return ERROR_INVALID_ARGUMENT; + } + + gpio_set_level(static_cast(state->pin_dc), dc_high ? 1 : 0); + if (state->pin_cs >= 0) { + gpio_set_level(static_cast(state->pin_cs), 0); + } + + spi_transaction_t transaction = {}; + transaction.length = size * 8; + if (size <= sizeof(transaction.tx_data)) { + // Short transfers avoid a DMA bounce buffer entirely. + transaction.flags |= SPI_TRANS_USE_TXDATA; + std::memcpy(transaction.tx_data, data, size); + } else { + transaction.tx_buffer = data; + } +#if SOC_PSRAM_DMA_CAPABLE + // A Gfx frame buffer usually lives in PSRAM. Ask the SPI DMA engine to read it directly + // instead of bouncing every chunk through a temporary internal buffer. The flag is harmless + // for buffers that turn out to be internal; the driver checks the address itself. + transaction.flags |= SPI_TRANS_DMA_USE_PSRAM; +#endif + + const esp_err_t result = spi_device_polling_transmit(state->device, &transaction); + if (state->pin_cs >= 0 && !keep_cs) { + gpio_set_level(static_cast(state->pin_cs), 1); + } + return result == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +/** Command byte, followed by its parameters (both with CS held across the boundary). */ +error_t writeCommand(State* state, uint8_t command, const uint8_t* parameters = nullptr, size_t parameter_size = 0) { + error_t error = transmit(state, &command, 1, parameter_size > 0, false); + if (error == ERROR_NONE && parameter_size > 0) { + error = transmit(state, parameters, parameter_size, false, true); + } + return error; +} + +error_t writePixels(State* state, const void* data, size_t size) { + const uint8_t command = CMD_RAMWR; + error_t error = transmit(state, &command, 1, true, false); + if (error != ERROR_NONE) { + return error; + } + + const auto* bytes = static_cast(data); + size_t offset = 0; + while (offset < size) { + const size_t chunk = std::min(state->max_transfer_bytes, size - offset); + const bool more = offset + chunk < size; + error = transmit(state, bytes + offset, chunk, more, true); + if (error != ERROR_NONE) { + return error; + } + offset += chunk; + } + return ERROR_NONE; +} + +/** Reads as many bytes as fit in `size`, discarding any extra bytes the panel returns. */ +error_t readData(State* state, uint8_t* buffer, size_t size) { + spi_transaction_t transaction = {}; + transaction.rxlength = size * 8; + transaction.rx_buffer = buffer; + transaction.length = 0; + + gpio_set_level(static_cast(state->pin_dc), 1); + if (state->pin_cs >= 0) { + gpio_set_level(static_cast(state->pin_cs), 0); + } + const esp_err_t result = spi_device_polling_transmit(state->device, &transaction); + if (state->pin_cs >= 0) { + gpio_set_level(static_cast(state->pin_cs), 1); + } + return result == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; +} + +error_t applyMadctl(State* state) { + return writeCommand(state, CMD_MADCTL, &state->madctl, 1); +} + +void pulseReset(const Ili9341SpiConfig* config) { + if (config->pin_reset < 0) { + return; + } + const auto pin = static_cast(config->pin_reset); + const int active = config->reset_active_high ? 1 : 0; + gpio_set_level(pin, active); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(pin, !active); + // The ILI9341 needs 120 ms after a hardware reset before it accepts SLPOUT. + vTaskDelay(pdMS_TO_TICKS(120)); +} + +error_t initialisePanel(State* state, const Ili9341SpiConfig* config) { + error_t error = ERROR_NONE; + + if (config->pin_reset >= 0) { + gpio_config_t reset_config = {}; + reset_config.mode = GPIO_MODE_OUTPUT; + reset_config.pin_bit_mask = 1ULL << config->pin_reset; + if (gpio_config(&reset_config) != ESP_OK) { + LOG_E(TAG, "Failed to configure the reset pin"); + return ERROR_RESOURCE; + } + pulseReset(config); + } + + // Always send a software reset as well: it is harmless after a hardware reset and covers a + // reset line that isn't actually connected, where the panel would otherwise stay asleep. + error = writeCommand(state, CMD_SWRESET); + if (error != ERROR_NONE) { + return error; + } + vTaskDelay(pdMS_TO_TICKS(120)); + + error = writeCommand(state, CMD_SLPOUT); + if (error != ERROR_NONE) { + return error; + } + vTaskDelay(pdMS_TO_TICKS(100)); + + error = applyMadctl(state); + if (error == ERROR_NONE) { + const uint8_t color_mode = 0x55; // 16 bits per pixel (RGB565) + error = writeCommand(state, CMD_COLMOD, &color_mode, 1); + } + + for (const auto& command : INIT_COMMANDS) { + if (error != ERROR_NONE) { + break; + } + error = writeCommand(state, command.command, command.data, command.data_size); + if (command.delay_ms != 0) { + vTaskDelay(pdMS_TO_TICKS(command.delay_ms)); + } + } + + if (error == ERROR_NONE) { + error = writeCommand(state, config->invert_color ? CMD_INVON : CMD_INVOFF); + } + if (error == ERROR_NONE) { + error = writeCommand(state, CMD_DISPON); + } + return error; +} + +bool readPanelId(State* state) { + uint8_t id[4] = {}; + const uint8_t command = CMD_RDDID; + // Read commands need CS held from the command byte through the returned data. + error_t error = transmit(state, &command, 1, true, false); + if (error == ERROR_NONE) { + error = readData(state, id, sizeof(id)); + } + + const bool id_matches = error == ERROR_NONE && id[1] == 0x93 && id[2] == 0x41; + if (id_matches) { + LOG_I(TAG, "Panel ID: %02X %02X %02X %02X", id[0], id[1], id[2], id[3]); + return true; + } + if (error == ERROR_NONE) { + LOG_I(TAG, "Panel ID: %02X %02X %02X %02X", id[0], id[1], id[2], id[3]); + } + + // Many ILI9341-compatible controllers don't implement RDDID and return zeros, while still + // answering register reads. Reading back a MADCTL we just wrote proves the panel is alive. + const uint8_t read_madctl = CMD_RDD_MADCTL; + uint8_t madctl[2] = {}; + if (transmit(state, &read_madctl, 1, true, false) == ERROR_NONE && readData(state, madctl, sizeof(madctl)) == ERROR_NONE) { + LOG_I(TAG, "MADCTL readback: %02X %02X (written %02X)", madctl[0], madctl[1], state->madctl); + if (madctl[0] == state->madctl) { + LOG_I(TAG, "Panel responds; RDDID is not implemented by this controller"); + return true; + } + } + + LOG_W(TAG, "Panel doesn't respond to register reads (check SDO/MISO and the panel control lines)"); + return false; +} + +/** + * Drives DC, RESET and CS high one at a time (3 seconds each) with a matching log line, so the + * adapter's control wires can be identified at the panel connector with a multimeter. Ends with + * RESET and CS released (high), then the caller re-initialises the panel. + */ +void probeControlPins(const Ili9341SpiConfig* config) { + struct Pin { + const char* name; + int pin; + }; + const Pin pins[] = { + { "DC", config->pin_dc }, + { "RESET", config->pin_reset }, + { "CS", config->pin_cs }, + }; + + LOG_W(TAG, "Panel didn't answer: identifying control pins (3 s each, measure at the panel)"); + for (const auto& probe : pins) { + if (probe.pin < 0) { + continue; + } + gpio_config_t output = {}; + output.mode = GPIO_MODE_OUTPUT; + output.pin_bit_mask = 1ULL << probe.pin; + if (gpio_config(&output) != ESP_OK) { + continue; + } + gpio_set_level(static_cast(probe.pin), 0); + } + + for (const auto& probe : pins) { + if (probe.pin < 0) { + continue; + } + LOG_I(TAG, "%s (GPIO%d) HIGH for 3 s", probe.name, probe.pin); + gpio_set_level(static_cast(probe.pin), 1); + vTaskDelay(pdMS_TO_TICKS(3000)); + gpio_set_level(static_cast(probe.pin), 0); + vTaskDelay(pdMS_TO_TICKS(500)); + } + + // Leave the control lines in their idle state. + if (config->pin_reset >= 0) { + gpio_set_level(static_cast(config->pin_reset), 1); + } + if (config->pin_cs >= 0) { + gpio_set_level(static_cast(config->pin_cs), 1); + } + if (config->pin_dc >= 0) { + gpio_set_level(static_cast(config->pin_dc), 0); + } +} + +error_t start(Device* device) { + const auto* config = getConfig(device); + if (config == nullptr || config->pin_cs < 0 || config->pin_dc < 0) { + LOG_E(TAG, "Missing CS/DC pin in the configuration"); + return ERROR_INVALID_ARGUMENT; + } + + auto* state = new (std::nothrow) State(); + if (state == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + state->pin_dc = config->pin_dc; + state->pin_cs = config->pin_cs; + state->max_transfer_bytes = config->max_transfer_bytes; + state->madctl = (config->swap_xy ? MADCTL_MV : 0) | + (config->mirror_x ? MADCTL_MX : 0) | + (config->mirror_y ? MADCTL_MY : 0) | + (config->bgr_order ? MADCTL_BGR : 0); + + gpio_config_t dc_config = {}; + dc_config.mode = GPIO_MODE_OUTPUT; + dc_config.pin_bit_mask = 1ULL << config->pin_dc; + if (gpio_config(&dc_config) != ESP_OK) { + LOG_E(TAG, "Failed to configure the DC pin"); + delete state; + return ERROR_RESOURCE; + } + gpio_set_level(static_cast(config->pin_dc), 0); + + // Chip select is driven as a plain GPIO rather than by the SPI peripheral. That keeps the + // whole panel interface under our control (and survives the bring-up probe re-configuring the + // pin), which also makes a stubborn panel easier to diagnose. + gpio_config_t cs_config = {}; + cs_config.mode = GPIO_MODE_OUTPUT; + cs_config.pin_bit_mask = 1ULL << config->pin_cs; + if (gpio_config(&cs_config) != ESP_OK) { + LOG_E(TAG, "Failed to configure the CS pin"); + delete state; + return ERROR_RESOURCE; + } + gpio_set_level(static_cast(config->pin_cs), 1); + + spi_device_interface_config_t device_config = {}; + device_config.mode = 0; + device_config.clock_speed_hz = static_cast(config->pixel_clock_hz); + // -1: CS is manual (see transmit()), so the SPI peripheral must not drive it. + device_config.spics_io_num = -1; + device_config.queue_size = 1; // polling transactions only + // Half duplex allows receive-only transactions (the panel ID read); full duplex rejects + // rxlength > length, which a write-only display otherwise never needs. + device_config.flags = SPI_DEVICE_HALFDUPLEX; + esp_err_t result = spi_bus_add_device( + static_cast(config->spi_host), + &device_config, + &state->device + ); + if (result != ESP_OK) { + LOG_E(TAG, "Failed to add the SPI device: %s", esp_err_to_name(result)); + delete state; + return ERROR_RESOURCE; + } + + // Keep the bus for this panel: command and pixel transactions rely on CS staying asserted + // between them, so no other device may interleave. + spi_device_acquire_bus(state->device, portMAX_DELAY); + + error_t error = initialisePanel(state, config); + if (error == ERROR_NONE && config->log_panel_id) { + if (!readPanelId(state) && config->probe_control_pins_on_id_failure) { + probeControlPins(config); + error = initialisePanel(state, config); + } + } + + if (error != ERROR_NONE) { + LOG_E(TAG, "Panel initialisation failed"); + spi_device_release_bus(state->device); + spi_bus_remove_device(state->device); + delete state; + return error; + } + + device_set_driver_data(device, state); + LOG_I( + TAG, + "Panel ready: %ux%u, SPI %u Hz", + static_cast(config->width), + static_cast(config->height), + static_cast(config->pixel_clock_hz) + ); + return ERROR_NONE; +} + +error_t stop(Device* device) { + auto* state = getState(device); + if (state == nullptr) { + return ERROR_NONE; + } + + spi_device_release_bus(state->device); + spi_bus_remove_device(state->device); + delete state; + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +error_t reset(Device* device) { + auto* state = getState(device); + if (state == nullptr) { + return ERROR_INVALID_STATE; + } + const auto* config = getConfig(device); + + if (config->pin_reset >= 0) { + pulseReset(config); + return ERROR_NONE; + } + + error_t error = writeCommand(state, CMD_SWRESET); + vTaskDelay(pdMS_TO_TICKS(120)); + return error; +} + +error_t init(Device* device) { + auto* state = getState(device); + if (state == nullptr) { + return ERROR_INVALID_STATE; + } + error_t error = writeCommand(state, CMD_SLPOUT); + if (error == ERROR_NONE) { + vTaskDelay(pdMS_TO_TICKS(100)); + error = applyMadctl(state); + } + return error; +} + +error_t drawBitmap( + Device* device, + int32_t x_start, + int32_t y_start, + int32_t x_end, + int32_t y_end, + const void* color_data +) { + auto* state = getState(device); + if (state == nullptr || color_data == nullptr) { + return ERROR_INVALID_STATE; + } + if (x_start >= x_end || y_start >= y_end) { + return ERROR_INVALID_ARGUMENT; + } + + x_start += state->gap_x; + x_end += state->gap_x; + y_start += state->gap_y; + y_end += state->gap_y; + + const uint8_t caset[4] = { + static_cast(x_start >> 8), + static_cast(x_start), + static_cast((x_end - 1) >> 8), + static_cast(x_end - 1), + }; + const uint8_t raset[4] = { + static_cast(y_start >> 8), + static_cast(y_start), + static_cast((y_end - 1) >> 8), + static_cast(y_end - 1), + }; + + error_t error = writeCommand(state, CMD_CASET, caset, sizeof(caset)); + if (error == ERROR_NONE) { + error = writeCommand(state, CMD_RASET, raset, sizeof(raset)); + } + if (error == ERROR_NONE) { + const size_t pixel_count = static_cast(x_end - x_start) * static_cast(y_end - y_start); + error = writePixels(state, color_data, pixel_count * sizeof(uint16_t)); + } + return error; +} + +error_t mirror(Device* device, bool x_axis, bool y_axis) { + auto* state = getState(device); + if (state == nullptr) { + return ERROR_INVALID_STATE; + } + state->madctl = static_cast( + x_axis ? (state->madctl | MADCTL_MX) : (state->madctl & ~MADCTL_MX) + ); + state->madctl = static_cast( + y_axis ? (state->madctl | MADCTL_MY) : (state->madctl & ~MADCTL_MY) + ); + return applyMadctl(state); +} + +error_t swapXy(Device* device, bool swap_axes) { + auto* state = getState(device); + if (state == nullptr) { + return ERROR_INVALID_STATE; + } + state->madctl = static_cast( + swap_axes ? (state->madctl | MADCTL_MV) : (state->madctl & ~MADCTL_MV) + ); + return applyMadctl(state); +} + +bool getSwapXy(Device* device) { + auto* state = getState(device); + return state != nullptr && (state->madctl & MADCTL_MV) != 0; +} + +bool getMirrorX(Device* device) { + auto* state = getState(device); + return state != nullptr && (state->madctl & MADCTL_MX) != 0; +} + +bool getMirrorY(Device* device) { + auto* state = getState(device); + return state != nullptr && (state->madctl & MADCTL_MY) != 0; +} + +error_t setGap(Device* device, int32_t x_gap, int32_t y_gap) { + auto* state = getState(device); + if (state == nullptr) { + return ERROR_INVALID_STATE; + } + state->gap_x = x_gap; + state->gap_y = y_gap; + return ERROR_NONE; +} + +error_t writeDeviceCommand(Device* device, uint8_t command) { + auto* state = getState(device); + if (state == nullptr) { + return ERROR_INVALID_STATE; + } + return writeCommand(state, command); +} + +int32_t getGapX(Device* device) { + auto* state = getState(device); + return state != nullptr ? state->gap_x : 0; +} + +int32_t getGapY(Device* device) { + auto* state = getState(device); + return state != nullptr ? state->gap_y : 0; +} + +error_t invertColor(Device* device, bool invert) { + return writeDeviceCommand(device, invert ? CMD_INVON : CMD_INVOFF); +} + +error_t dispOnOff(Device* device, bool on) { + return writeDeviceCommand(device, on ? CMD_DISPON : CMD_DISPOFF); +} + +error_t dispSleep(Device* device, bool sleep) { + error_t error = writeDeviceCommand(device, sleep ? CMD_SLPIN : CMD_SLPOUT); + if (error == ERROR_NONE) { + vTaskDelay(pdMS_TO_TICKS(120)); + } + return error; +} + +enum DisplayColorFormat getColorFormat(Device*) { + // The panel expects each 16-bit pixel high-byte-first over SPI, which is the swap-invariant + // layout of DISPLAY_COLOR_FORMAT_RGB565_SWAPPED. Gfx then writes swapped values straight to + // memory and the bus sends them unmodified. + return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED; +} + +uint16_t getResolutionX(Device* device) { + return getConfig(device)->width; +} + +uint16_t getResolutionY(Device* device) { + return getConfig(device)->height; +} + +error_t getBacklight(Device*, Device**) { + return ERROR_NOT_SUPPORTED; +} + +const DisplayApi ILI9341_SPI_DISPLAY_API = { + .capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY | + DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | + DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLEEP, + .reset = reset, + .init = init, + .draw_bitmap = drawBitmap, + .clear = nullptr, + .refresh = nullptr, + .mirror = mirror, + .swap_xy = swapXy, + .get_swap_xy = getSwapXy, + .get_mirror_x = getMirrorX, + .get_mirror_y = getMirrorY, + .set_gap = setGap, + .get_gap_x = getGapX, + .get_gap_y = getGapY, + .invert_color = invertColor, + .disp_on_off = dispOnOff, + .disp_sleep = dispSleep, + .get_color_format = getColorFormat, + .get_resolution_x = getResolutionX, + .get_resolution_y = getResolutionY, + .get_frame_buffer = nullptr, + .get_frame_buffer_count = nullptr, + .get_backlight = getBacklight, + .has_capability = nullptr, + .get_shape = nullptr, +}; + +} // namespace + +extern "C" { + +Driver ili9341_spi_driver = { + .name = "ili9341-spi", + .compatible = (const char*[]) { "tactilityapps,ili9341-spi", nullptr }, + .start_device = start, + .stop_device = stop, + .probe = nullptr, + .api = &ILI9341_SPI_DISPLAY_API, + .device_type = &DISPLAY_TYPE, + .internal = nullptr, +}; + +} // extern "C" diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.h b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.h new file mode 100644 index 0000000..3dd7fa7 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include + +/** + * ILI9341 SPI display driver. + * + * Used by the `ili9341-panel-module` kernel module in `Apps/MystifySpiDemo`, which owns the SPI + * bus, creates the display device from persisted settings, and tears it down again. The module is + * loaded from the app package at boot, so the panel exists as a normal DisplayApi device even when + * no app is running. + * + * The driver uses ESP-IDF's spi_master polling transactions directly instead of esp_lcd. That + * makes draw_bitmap() fully synchronous with no completion callback: an app/module ELF is not + * guaranteed to be executable while the firmware's SPI ISR runs with caches disabled, and a + * callback into it could crash there. + */ + +/** What the panel looks like after MADCTL has been applied (its logical resolution). */ +struct Ili9341SpiConfig { + /** ESP-IDF SPI host that is already initialised with the panel's bus pins (e.g. SPI2_HOST). */ + int spi_host = 1; + + uint16_t width = 320; + uint16_t height = 240; + + /** MADCTL row/column exchange. Together with width/height this selects the landscape mode. */ + bool swap_xy = true; + bool mirror_x = false; + bool mirror_y = false; + /** Send INVON. If the image looks like a photographic negative, this is the knob. */ + bool invert_color = true; + /** BGR element order, as used by M5Stack/LCDWiki modules. */ + bool bgr_order = true; + + uint32_t pixel_clock_hz = 40 * 1000 * 1000; + + /** Largest single SPI transaction. Must not exceed the bus' max_transfer_sz. */ + uint16_t max_transfer_bytes = 16 * 1024; + + /** Panel control pins. Set to -1 when not wired. */ + int pin_dc = -1; + int pin_reset = -1; + int pin_cs = -1; + bool reset_active_high = false; + + /** Read and log the panel ID once at startup (needs MISO wired). */ + bool log_panel_id = true; + + /** + * Bring-up aid: when the panel doesn't answer, drive DC/RESET/CS one at a time so they can be + * identified with a multimeter at the panel connector (verifies the adapter's pin mapping). + */ + bool probe_control_pins_on_id_failure = false; +}; + +/** The driver instance. Register with driver_construct_add() and unregister on shutdown. */ +extern "C" Driver ili9341_spi_driver; + +/** Compatible string for device_construct_add_start(). */ +inline const char* const ILI9341_SPI_COMPATIBLE = "tactilityapps,ili9341-spi"; diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.cpp b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.cpp new file mode 100644 index 0000000..5a1b5b1 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.cpp @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "Panel.h" + +#include "Ili9341Spi.h" + +#include +#include +#include +#include +#include + +#include + +namespace ili9341panel { + +namespace { + +constexpr auto* TAG = "Ili9341Panel"; +constexpr auto* DEVICE_NAME = "ili9341-panel"; + +struct PanelRuntime { + struct Device device {}; + struct Ili9341SpiConfig driver_config {}; + bool bus_initialized = false; + bool device_added = false; +}; + +PanelRuntime runtime; + +} // namespace + +error_t startPanel(const struct Ili9341PanelConfig& config) { + if (!config.enabled || runtime.device_added) { + return ERROR_NONE; + } + + spi_bus_config_t bus = {}; + bus.mosi_io_num = config.pin_mosi; + bus.miso_io_num = config.pin_miso; + bus.sclk_io_num = config.pin_sck; + bus.quadwp_io_num = -1; + bus.quadhd_io_num = -1; + bus.data4_io_num = -1; + bus.data5_io_num = -1; + bus.data6_io_num = -1; + bus.data7_io_num = -1; + bus.data_io_default_level = false; + bus.max_transfer_sz = static_cast(config.max_transfer_bytes); + bus.dma_burst_size = 0; + bus.flags = 0; + bus.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO; + bus.intr_flags = 0; + + const auto host = static_cast(config.spi_host); + esp_err_t bus_result = spi_bus_initialize(host, &bus, SPI_DMA_CH_AUTO); + if (bus_result != ESP_OK) { + LOG_E(TAG, "spi_bus_initialize failed: %s", esp_err_to_name(bus_result)); + return ERROR_RESOURCE; + } + runtime.bus_initialized = true; + + runtime.driver_config = Ili9341SpiConfig { + .spi_host = config.spi_host, + .width = config.width, + .height = config.height, + .swap_xy = config.swap_xy, + .mirror_x = config.mirror_x, + .mirror_y = config.mirror_y, + .invert_color = config.invert_color, + .bgr_order = config.bgr_order, + .pixel_clock_hz = config.pixel_clock_hz, + .max_transfer_bytes = static_cast(config.max_transfer_bytes), + .pin_dc = config.pin_dc, + .pin_reset = config.pin_reset, + .pin_cs = config.pin_cs, + .reset_active_high = false, + .log_panel_id = true, + .probe_control_pins_on_id_failure = false, + }; + + runtime.device = {}; + runtime.device.name = DEVICE_NAME; + runtime.device.address = 0; + runtime.device.config = &runtime.driver_config; + runtime.device.parent = nullptr; + runtime.device.flags = 0; + + if (device_construct_add_start(&runtime.device, ILI9341_SPI_COMPATIBLE) != ERROR_NONE) { + LOG_E(TAG, "Failed to start the ILI9341 panel"); + spi_bus_free(host); + runtime.bus_initialized = false; + return ERROR_RESOURCE; + } + runtime.device_added = true; + + LOG_I( + TAG, + "Panel ready: %ux%u, SPI %u Hz", + static_cast(config.width), + static_cast(config.height), + static_cast(config.pixel_clock_hz) + ); + return ERROR_NONE; +} + +error_t stopPanel() { + error_t result = ERROR_NONE; + + if (runtime.device_added) { + error_t stop_result = device_stop(&runtime.device); + error_t remove_result = stop_result == ERROR_NONE ? device_remove(&runtime.device) : ERROR_INVALID_STATE; + error_t destruct_result = remove_result == ERROR_NONE ? device_destruct(&runtime.device) : ERROR_INVALID_STATE; + if (destruct_result != ERROR_NONE) { + LOG_W(TAG, "Could not tear down the display device (still in use)"); + result = ERROR_RESOURCE_BUSY; + } else { + runtime.device_added = false; + } + } + + if (result == ERROR_NONE && runtime.bus_initialized) { + const auto host = static_cast(runtime.driver_config.spi_host); + if (spi_bus_free(host) != ESP_OK) { + LOG_W(TAG, "Could not free the SPI bus"); + result = ERROR_RESOURCE; + } else { + runtime.bus_initialized = false; + } + } + + return result; +} + +bool isPanelActive() { + return runtime.device_added && device_is_ready(&runtime.device); +} + +struct Device* getPanelDevice() { + if (!runtime.device_added) { + return nullptr; + } + if (device_get(&runtime.device) != ERROR_NONE) { + return nullptr; + } + return &runtime.device; +} + +} // namespace ili9341panel diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.h b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.h new file mode 100644 index 0000000..36d36e6 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +namespace ili9341panel { + +/** Creates the SPI bus and the display device from @a config. Does nothing when disabled. */ +error_t startPanel(const struct Ili9341PanelConfig& config); + +/** Stops and tears down the device and the bus. */ +error_t stopPanel(); + +bool isPanelActive(); + +/** @return a new device reference, or nullptr when the panel is not active. */ +struct Device* getPanelDevice(); + +} // namespace ili9341panel diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Settings.cpp b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Settings.cpp new file mode 100644 index 0000000..bf24765 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Settings.cpp @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "Settings.h" + +#include +#include +#include + +#include + +namespace ili9341panel { + +namespace { + +constexpr auto* TAG = "Ili9341PanelSettings"; + +// The package id: the app in this package shares the same user data directory. +constexpr auto* PACKAGE_ID = "tactility.mystifyspidemo"; +constexpr auto* FILE_NAME = "ili9341panel.properties"; + +// SPI2_HOST on ESP32-P4. +constexpr int32_t DEFAULT_SPI_HOST = 1; + +class SettingsFile { +public: + SettingsFile() { + char path[256]; + if (app_paths_get_user_data_path(PACKAGE_ID, FILE_NAME, path, sizeof(path)) != ERROR_NONE) { + LOG_E(TAG, "Could not resolve the settings path"); + return; + } + preferences_ = preferences_open(path); + } + + ~SettingsFile() { + if (preferences_ != nullptr) { + preferences_close(preferences_); + } + } + + SettingsFile(const SettingsFile&) = delete; + SettingsFile& operator=(const SettingsFile&) = delete; + + bool valid() const { return preferences_ != nullptr; } + + Preferences* get() { return preferences_; } + +private: + Preferences* preferences_ = nullptr; +}; + +void loadBool(Preferences* preferences, const char* key, bool& value) { + bool loaded = false; + if (preferences_opt_bool(preferences, key, &loaded)) { + value = loaded; + } +} + +void loadInt32(Preferences* preferences, const char* key, int32_t& value) { + int32_t loaded = 0; + if (preferences_opt_int32(preferences, key, &loaded)) { + value = loaded; + } +} + +void loadUint16(Preferences* preferences, const char* key, uint16_t& value) { + int32_t loaded = 0; + if (preferences_opt_int32(preferences, key, &loaded)) { + value = static_cast(loaded); + } +} + +void loadUint32(Preferences* preferences, const char* key, uint32_t& value) { + int32_t loaded = 0; + if (preferences_opt_int32(preferences, key, &loaded)) { + value = static_cast(loaded); + } +} + +} // namespace + +error_t loadSettings(struct Ili9341PanelConfig& config) { + const error_t defaults_result = ili9341panel_settings_defaults(&config); + if (defaults_result != ERROR_NONE) { + return defaults_result; + } + + SettingsFile file; + if (!file.valid()) { + // No storage yet: run from the defaults. + return ERROR_NONE; + } + + Preferences* preferences = file.get(); + loadBool(preferences, "enabled", config.enabled); + loadInt32(preferences, "spiHost", config.spi_host); + loadInt32(preferences, "pinSck", config.pin_sck); + loadInt32(preferences, "pinMosi", config.pin_mosi); + loadInt32(preferences, "pinMiso", config.pin_miso); + loadInt32(preferences, "pinDc", config.pin_dc); + loadInt32(preferences, "pinReset", config.pin_reset); + loadInt32(preferences, "pinCs", config.pin_cs); + loadUint16(preferences, "width", config.width); + loadUint16(preferences, "height", config.height); + loadBool(preferences, "swapXy", config.swap_xy); + loadBool(preferences, "mirrorX", config.mirror_x); + loadBool(preferences, "mirrorY", config.mirror_y); + loadBool(preferences, "invertColor", config.invert_color); + loadBool(preferences, "bgrOrder", config.bgr_order); + loadUint32(preferences, "pixelClockHz", config.pixel_clock_hz); + loadUint32(preferences, "maxTransferBytes", config.max_transfer_bytes); + + if (config.width == 0 || config.height == 0 || config.pixel_clock_hz == 0 || config.max_transfer_bytes == 0) { + LOG_W(TAG, "Ignoring invalid saved settings"); + return ili9341panel_settings_defaults(&config); + } + return ERROR_NONE; +} + +error_t saveSettings(const struct Ili9341PanelConfig& config) { + SettingsFile file; + if (!file.valid()) { + return ERROR_RESOURCE; + } + + Preferences* preferences = file.get(); + preferences_put_bool(preferences, "enabled", config.enabled); + preferences_put_int32(preferences, "spiHost", config.spi_host); + preferences_put_int32(preferences, "pinSck", config.pin_sck); + preferences_put_int32(preferences, "pinMosi", config.pin_mosi); + preferences_put_int32(preferences, "pinMiso", config.pin_miso); + preferences_put_int32(preferences, "pinDc", config.pin_dc); + preferences_put_int32(preferences, "pinReset", config.pin_reset); + preferences_put_int32(preferences, "pinCs", config.pin_cs); + preferences_put_int32(preferences, "width", config.width); + preferences_put_int32(preferences, "height", config.height); + preferences_put_bool(preferences, "swapXy", config.swap_xy); + preferences_put_bool(preferences, "mirrorX", config.mirror_x); + preferences_put_bool(preferences, "mirrorY", config.mirror_y); + preferences_put_bool(preferences, "invertColor", config.invert_color); + preferences_put_bool(preferences, "bgrOrder", config.bgr_order); + preferences_put_int32(preferences, "pixelClockHz", static_cast(config.pixel_clock_hz)); + preferences_put_int32(preferences, "maxTransferBytes", static_cast(config.max_transfer_bytes)); + return ERROR_NONE; +} + +} // namespace ili9341panel + +extern "C" error_t ili9341panel_settings_defaults(struct Ili9341PanelConfig* out_config) { + if (out_config == nullptr) { + return ERROR_INVALID_ARGUMENT; + } + *out_config = Ili9341PanelConfig { + .enabled = true, + .spi_host = ili9341panel::DEFAULT_SPI_HOST, + .pin_sck = 5, + .pin_mosi = 18, + .pin_miso = 19, + .pin_dc = 47, + .pin_reset = 4, + .pin_cs = 51, + .width = 320, + .height = 240, + .swap_xy = true, + .mirror_x = false, + .mirror_y = false, + .invert_color = false, + .bgr_order = true, + .pixel_clock_hz = 40 * 1000 * 1000, + .max_transfer_bytes = 16 * 1024, + }; + return ERROR_NONE; +} + +extern "C" error_t ili9341panel_settings_load(struct Ili9341PanelConfig* out_config) { + if (out_config == nullptr) { + return ERROR_INVALID_ARGUMENT; + } + return ili9341panel::loadSettings(*out_config); +} + +extern "C" error_t ili9341panel_settings_save(const struct Ili9341PanelConfig* config) { + if (config == nullptr) { + return ERROR_INVALID_ARGUMENT; + } + return ili9341panel::saveSettings(*config); +} diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Settings.h b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Settings.h new file mode 100644 index 0000000..5574cd2 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Settings.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +namespace ili9341panel { + +/** Loads the saved settings, falling back to the defaults per field. */ +error_t loadSettings(struct Ili9341PanelConfig& config); + +/** Writes every field to the properties file. */ +error_t saveSettings(const struct Ili9341PanelConfig& config); + +} // namespace ili9341panel diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/module.cpp b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/module.cpp new file mode 100644 index 0000000..f8d39f4 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/module.cpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "Panel.h" +#include "Settings.h" + +#include "Ili9341Spi.h" +#include + +#include +#include +#include +#include +#include + +constexpr auto* TAG = "Ili9341PanelModule"; + +extern "C" { + +/** + * The driver is registered manually instead of through Module.drivers: the kernel calls start() + * *before* it constructs the drivers in that list, and the device can only be created afterwards. + */ +static error_t start() { + if (driver_construct_add(&ili9341_spi_driver) != ERROR_NONE) { + LOG_E(TAG, "Could not register the ILI9341 driver"); + return ERROR_RESOURCE; + } + + struct Ili9341PanelConfig config {}; + error_t settings_result = ili9341panel_settings_load(&config); + if (settings_result != ERROR_NONE) { + LOG_E(TAG, "Could not load settings; using defaults"); + ili9341panel_settings_defaults(&config); + } + + // A failure here leaves the module running so the settings app can fix the configuration. + if (ili9341panel::startPanel(config) != ERROR_NONE) { + LOG_E(TAG, "Panel not started; check the saved settings"); + } + return ERROR_NONE; +} + +static error_t stop() { + error_t panel_result = ili9341panel::stopPanel(); + if (panel_result != ERROR_NONE) { + // Keep the driver registered: the device (and its driver) is still in use. + return panel_result; + } + if (driver_remove_destruct(&ili9341_spi_driver) != ERROR_NONE) { + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +error_t ili9341panel_apply() { + struct Ili9341PanelConfig config {}; + error_t result = ili9341panel_settings_load(&config); + if (result != ERROR_NONE) { + return result; + } + error_t stop_result = ili9341panel::stopPanel(); + if (stop_result != ERROR_NONE) { + return stop_result; + } + return ili9341panel::startPanel(config); +} + +bool ili9341panel_is_active() { + return ili9341panel::isPanelActive(); +} + +error_t ili9341panel_get_device(struct Device** out_device) { + if (out_device == nullptr) { + return ERROR_INVALID_ARGUMENT; + } + struct Device* device = ili9341panel::getPanelDevice(); + if (device == nullptr) { + return ERROR_NOT_FOUND; + } + *out_device = device; + return ERROR_NONE; +} + +static const ModuleSymbol symbols[] = { + DEFINE_MODULE_SYMBOL(ili9341panel_settings_defaults), + DEFINE_MODULE_SYMBOL(ili9341panel_settings_load), + DEFINE_MODULE_SYMBOL(ili9341panel_settings_save), + DEFINE_MODULE_SYMBOL(ili9341panel_apply), + DEFINE_MODULE_SYMBOL(ili9341panel_is_active), + DEFINE_MODULE_SYMBOL(ili9341panel_get_device), + MODULE_SYMBOL_TERMINATOR +}; + +static Module module = { + .name = "ili9341-panel", + .start = start, + .stop = stop, + .drivers = nullptr, + .symbols = symbols, + .internal = nullptr, +}; + +// The entry point of a module returns its struct Module. The asm label keeps the symbol name of +// main for another return type. +#define ENTRY_NAME_INNER(name) #name +#define ENTRY_NAME(name) ENTRY_NAME_INNER(name) +Module* module_main(int argc, char* argv[]) __asm__(ENTRY_NAME(main)); + +Module* module_main(int argc, char* argv[]) { + return &module; +} + +} diff --git a/Apps/MystifySpiDemo/ili9341-panel-settings/CMakeLists.txt b/Apps/MystifySpiDemo/ili9341-panel-settings/CMakeLists.txt new file mode 100644 index 0000000..65ee577 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-settings/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(tactility.mystifyspidemo.ili9341-panel-settings) +project(tactility.mystifyspidemo.ili9341-panel-settings) +tactility_project_post(tactility.mystifyspidemo.ili9341-panel-settings) diff --git a/Apps/MystifySpiDemo/ili9341-panel-settings/main/CMakeLists.txt b/Apps/MystifySpiDemo/ili9341-panel-settings/main/CMakeLists.txt new file mode 100644 index 0000000..b26d03e --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-settings/main/CMakeLists.txt @@ -0,0 +1,8 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +tactility_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS ../../common +) diff --git a/Apps/MystifySpiDemo/ili9341-panel-settings/main/Source/main.cpp b/Apps/MystifySpiDemo/ili9341-panel-settings/main/Source/main.cpp new file mode 100644 index 0000000..e2cdb8c --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-settings/main/Source/main.cpp @@ -0,0 +1,1320 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "panel_display.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr int32_t REQUEST_NONE = -1; +constexpr int32_t REQUEST_SAVE = 0; +constexpr int32_t REQUEST_APPLY = 1; +constexpr int32_t REQUEST_REVERT = 2; +constexpr int32_t REQUEST_SET_ENABLED = 3; +/** Posted by buildUi() when the module was started elsewhere (e.g. Kernel settings) while the window was hidden. */ +constexpr int32_t REQUEST_REFRESH = 4; +/** Opens the input dialog for a numeric row on the app task. */ +constexpr int32_t REQUEST_EDIT_NUMBER = 5; + +// This app ships in the same package as the module, which is how the enable switch can start and +// stop it (so the state matches the one in Kernel settings). +constexpr const char* PACKAGE_ID = "tactility.mystifyspidemo"; +constexpr const char* MODULE_NAME = "ili9341-panel"; +constexpr const char* INPUT_DIALOG_APP_ID = "tactility.inputdialog"; + +constexpr uint32_t CLOCK_PRESETS[] = { 10, 20, 40, 80 }; // MHz +constexpr uint32_t CLOCK_PRESET_COUNT = sizeof(CLOCK_PRESETS) / sizeof(CLOCK_PRESETS[0]); +constexpr uint32_t HOST_PRESETS[] = { 1, 2 }; // SPI2_HOST, SPI3_HOST +constexpr uint32_t HOST_PRESET_COUNT = sizeof(HOST_PRESETS) / sizeof(HOST_PRESETS[0]); + +/** The settings whose values are entered through the input dialog. */ +enum class NumericField : uint8_t { + Width, + Height, + MaxTransfer, + PinSck, + PinMosi, + PinMiso, + PinDc, + PinReset, + PinCs, + Count, +}; + +constexpr size_t NUMERIC_FIELD_COUNT = static_cast(NumericField::Count); + +struct NumberDialog { + NumericField field = NumericField::Width; + AppInstanceId instanceId = 0; + AppStream stream {}; + char buffer[32] = {}; +}; + +struct PanelApi { + decltype(&ili9341panel_settings_defaults) defaults = nullptr; + decltype(&ili9341panel_settings_load) load = nullptr; + decltype(&ili9341panel_settings_save) save = nullptr; + decltype(&ili9341panel_apply) apply = nullptr; + decltype(&ili9341panel_is_active) isActive = nullptr; + decltype(&ili9341panel_get_device) getDevice = nullptr; + Module* module = nullptr; +}; + +struct Context { + AppInstanceId appInstanceId = 0; + PanelApi api {}; + bool moduleAvailable = false; + + // Guards slow work (saving/applying) out of the LVGL task. + TaskEventGroup* eventGroup = nullptr; + uint32_t requestBit = 0; + std::atomic request { REQUEST_NONE }; + std::atomic enableTarget { false }; + /** NumericField chosen by a row's change button, started as a dialog by the app task. */ + std::atomic editField { -1 }; + + // Written by LVGL callbacks (LVGL lock held), read by the app task under the same lock. + Ili9341PanelConfig config {}; + + lv_obj_t* statusLabel = nullptr; + lv_obj_t* actionStatusLabel = nullptr; + lv_obj_t* stoppedHint = nullptr; + lv_obj_t* switchEnable = nullptr; + lv_obj_t* switchMirrorX = nullptr; + lv_obj_t* switchMirrorY = nullptr; + lv_obj_t* switchInvert = nullptr; + lv_obj_t* switchBgr = nullptr; + lv_obj_t* orientationChips[4] = {}; + lv_obj_t* resolutionChips[2] = {}; + lv_obj_t* clockChips[CLOCK_PRESET_COUNT] = {}; + lv_obj_t* hostChips[HOST_PRESET_COUNT] = {}; + lv_obj_t* valueLabels[NUMERIC_FIELD_COUNT] = {}; + NumberDialog dialog {}; +}; + +// region Module API + +bool resolveSymbol(const char* name, uintptr_t* address, Module** owner) { + return module_resolve_symbol_global_get(name, address, owner); +} + +bool acquireApi(PanelApi& api) { + Module* module = nullptr; + + auto resolve = [&module](const char* name, auto& function) { + uintptr_t address = 0; + Module* owner = nullptr; + if (!resolveSymbol(name, &address, &owner)) { + return false; + } + function = reinterpret_cast>(address); + if (module == nullptr) { + module = owner; + } else { + module_put(owner); + } + return true; + }; + + const bool found = resolve("ili9341panel_settings_defaults", api.defaults) && + resolve("ili9341panel_settings_load", api.load) && + resolve("ili9341panel_settings_save", api.save) && + resolve("ili9341panel_apply", api.apply) && + resolve("ili9341panel_is_active", api.isActive) && + resolve("ili9341panel_get_device", api.getDevice); + + if (!found) { + if (module != nullptr) { + module_put(module); + } + api = PanelApi {}; + return false; + } + + // Symbol lookup also finds stopped modules (their code stays loaded), so check the state too: + // otherwise a stopped panel would look "available" and the settings would silently be saved + // for a device that isn't there. + if (!module_is_started(module)) { + module_put(module); + api = PanelApi {}; + return false; + } + + api.module = module; + return true; +} + +void releaseApi(PanelApi& api) { + if (api.module != nullptr) { + module_put(api.module); + } + // Clearing the function pointers also makes sure a stale pointer can't be called after the + // module was stopped and unloaded. + api = PanelApi {}; +} + +/** Mirrors ili9341panel_settings_defaults(), so the form is usable while the module is stopped. */ +void setFallbackDefaults(Ili9341PanelConfig& config) { + config = Ili9341PanelConfig { + .enabled = true, + .spi_host = 1, + .pin_sck = 5, + .pin_mosi = 18, + .pin_miso = 19, + .pin_dc = 47, + .pin_reset = 4, + .pin_cs = 51, + .width = 320, + .height = 240, + .swap_xy = true, + .mirror_x = false, + .mirror_y = false, + .invert_color = false, + .bgr_order = true, + .pixel_clock_hz = 40 * 1000 * 1000, + .max_transfer_bytes = 16 * 1024, + }; +} + +// endregion + +// region LVGL <-> module glue + +/** Removes the panel's lv_display so the device can be stopped/drawn to. Must not hold the LVGL lock. */ +void detachPanelDisplay(Context& ctx, bool clear = false) { + struct Device* device = nullptr; + if (ctx.api.getDevice(&device) != ERROR_NONE || device == nullptr) { + return; + } + ili9341panelui::detachFromLvgl(device, clear); + device_put(device); +} + +/** Re-adds the panel as an LVGL display, using the same config as the lvgl module's device scan. */ +error_t attachPanelDisplay(Context& ctx) { + struct Device* device = nullptr; + error_t result = ctx.api.getDevice(&device); + if (result != ERROR_NONE || device == nullptr) { + return result == ERROR_NONE ? ERROR_NOT_FOUND : result; + } + result = ili9341panelui::attachToLvgl(device); + device_put(device); + return result; +} + +// endregion + +// region UI helpers + +lv_obj_t* createCard(lv_obj_t* parent, lv_coord_t gap) { + auto* card = lvgl_card_create(parent); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_gap(card, gap, LV_STATE_DEFAULT); + lv_obj_set_scrollable(card, false); + return card; +} + +lv_obj_t* createRow(lv_obj_t* parent, lv_coord_t gap) { + auto* row = lv_obj_create(parent); + lv_obj_remove_style_all(row); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW_WRAP); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_gap(row, gap, LV_STATE_DEFAULT); + lv_obj_set_scrollable(row, false); + return row; +} + +lv_obj_t* createChip(lv_obj_t* parent, const char* text, lv_coord_t minWidth) { + auto* chip = lvgl_chip_create(parent); + if (minWidth > 0) { + lv_obj_set_style_min_width(chip, minWidth, LV_STATE_DEFAULT); + } + auto* label = lv_label_create(chip); + lv_label_set_text(label, text); + lv_obj_center(label); + return chip; +} + +lv_obj_t* createSectionTitle(lv_obj_t* parent, const char* text) { + auto* label = lv_label_create(parent); + lv_label_set_text(label, text); + lv_obj_set_style_text_font(label, lvgl_get_text_font(FONT_SIZE_LARGE), LV_STATE_DEFAULT); + return label; +} + +lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* text, bool checked, lv_coord_t gap) { + auto* row = createRow(parent, gap); + auto* label = lv_label_create(row); + lv_label_set_text(label, text); + lv_obj_set_flex_grow(label, 1); + auto* control = lv_switch_create(row); + if (checked) { + lv_obj_add_state(control, LV_STATE_CHECKED); + } + return control; +} + +void onValueRowPressed(lv_event_t* event); + +/** Creates a mesh-settings style row: title and value on the left, a change button on the right. */ +lv_obj_t* createValueRow(lv_obj_t* parent, Context& ctx, const char* title, NumericField field, lv_coord_t gap) { + auto* row = createRow(parent, gap); + + auto* texts = lv_obj_create(row); + lv_obj_remove_style_all(texts); + lv_obj_set_height(texts, LV_SIZE_CONTENT); + lv_obj_set_flex_grow(texts, 1); + lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_gap(texts, 0, LV_STATE_DEFAULT); + lv_obj_set_scrollable(texts, false); + + auto* titleLabel = lv_label_create(texts); + lv_obj_set_width(titleLabel, LV_PCT(100)); + lv_label_set_long_mode(titleLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + lv_label_set_text(titleLabel, title); + + auto* valueLabel = lv_label_create(texts); + lv_obj_set_width(valueLabel, LV_PCT(100)); + lv_obj_set_style_text_font(valueLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT); + lv_label_set_long_mode(valueLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + + auto* button = lv_button_create(row); + lv_obj_set_user_data(button, reinterpret_cast(static_cast(field))); + auto* icon = lv_label_create(button); + lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT); + lv_label_set_text(icon, LVGL_ICON_SHARED_LIST_ALT); + lv_obj_center(icon); + lv_obj_add_event_cb(button, onValueRowPressed, LV_EVENT_SHORT_CLICKED, &ctx); + + return valueLabel; +} + +void setChipSelected(lv_obj_t* chip, bool selected) { + if (chip != nullptr) { + lv_obj_set_state(chip, LV_STATE_CHECKED, selected); + } +} + +int32_t getNumericField(const Context& ctx, NumericField field) { + switch (field) { + case NumericField::Width: + return ctx.config.width; + case NumericField::Height: + return ctx.config.height; + case NumericField::MaxTransfer: + // Stored in bytes, edited in KB; rounded so 65535 still shows as 64 KB. + return static_cast((ctx.config.max_transfer_bytes + 512) / 1024); + case NumericField::PinSck: + return ctx.config.pin_sck; + case NumericField::PinMosi: + return ctx.config.pin_mosi; + case NumericField::PinMiso: + return ctx.config.pin_miso; + case NumericField::PinDc: + return ctx.config.pin_dc; + case NumericField::PinReset: + return ctx.config.pin_reset; + case NumericField::PinCs: + return ctx.config.pin_cs; + default: + return 0; + } +} + +void setNumericField(Context& ctx, NumericField field, int32_t value) { + switch (field) { + case NumericField::Width: + ctx.config.width = static_cast(value); + break; + case NumericField::Height: + ctx.config.height = static_cast(value); + break; + case NumericField::MaxTransfer: { + const uint32_t bytes = static_cast(value) * 1024; + ctx.config.max_transfer_bytes = bytes > 65535 ? 65535 : bytes; + break; + } + case NumericField::PinSck: + ctx.config.pin_sck = value; + break; + case NumericField::PinMosi: + ctx.config.pin_mosi = value; + break; + case NumericField::PinMiso: + ctx.config.pin_miso = value; + break; + case NumericField::PinDc: + ctx.config.pin_dc = value; + break; + case NumericField::PinReset: + ctx.config.pin_reset = value; + break; + case NumericField::PinCs: + ctx.config.pin_cs = value; + break; + default: + break; + } +} + +/** Updates one value label from the working config. Call with the LVGL lock held. */ +void updateValueLabel(Context& ctx, NumericField field) { + auto* label = ctx.valueLabels[static_cast(field)]; + if (label == nullptr) { + return; + } + const int32_t value = getNumericField(ctx, field); + if (field == NumericField::MaxTransfer) { + lv_label_set_text_fmt(label, "%d KB", static_cast(value)); + } else if (value < 0) { + lv_label_set_text(label, "None"); + } else { + lv_label_set_text_fmt(label, "%d", static_cast(value)); + } +} + +int orientationIndex(const Ili9341PanelConfig& config) { + if (config.swap_xy && !config.mirror_x && !config.mirror_y) return 0; // landscape + if (!config.swap_xy && !config.mirror_x && config.mirror_y) return 1; // portrait + if (config.swap_xy && config.mirror_x && config.mirror_y) return 2; // landscape, 180 + if (!config.swap_xy && config.mirror_x && !config.mirror_y) return 3; // portrait, 180 + return -1; +} + +void applyOrientation(Ili9341PanelConfig& config, int index) { + switch (index) { + case 0: + config.swap_xy = true; + config.mirror_x = false; + config.mirror_y = false; + break; + case 1: + // This panel needs mirror Y for portrait; without it the image is upside down. + config.swap_xy = false; + config.mirror_x = false; + config.mirror_y = true; + break; + case 2: + config.swap_xy = true; + config.mirror_x = true; + config.mirror_y = true; + break; + case 3: + // ...and mirror X for portrait 180. + config.swap_xy = false; + config.mirror_x = true; + config.mirror_y = false; + break; + default: + break; + } +} + +void refreshOrientationChips(Context& ctx) { + const int selected = orientationIndex(ctx.config); + for (int i = 0; i < 4; i++) { + setChipSelected(ctx.orientationChips[i], i == selected); + } +} + +/** Highlights the size preset that matches the working width/height. Call with the LVGL lock held. */ +void refreshResolutionChips(Context& ctx) { + setChipSelected(ctx.resolutionChips[0], ctx.config.width == 320 && ctx.config.height == 240); + setChipSelected(ctx.resolutionChips[1], ctx.config.width == 240 && ctx.config.height == 320); +} + +void refreshClockChips(Context& ctx) { + for (uint32_t i = 0; i < CLOCK_PRESET_COUNT; i++) { + setChipSelected(ctx.clockChips[i], ctx.config.pixel_clock_hz == CLOCK_PRESETS[i] * 1000 * 1000); + } +} + +void refreshHostChips(Context& ctx) { + for (uint32_t i = 0; i < HOST_PRESET_COUNT; i++) { + setChipSelected(ctx.hostChips[i], ctx.config.spi_host == static_cast(HOST_PRESETS[i])); + } +} + +/** Writes the working config into every widget. Call with the LVGL lock held. */ +void writeConfigToWidgets(Context& ctx) { + if (ctx.switchEnable != nullptr) { + // The switch mirrors the module's state, so it always matches Kernel settings. + lv_obj_set_state(ctx.switchEnable, LV_STATE_CHECKED, ctx.moduleAvailable); + } + if (ctx.switchMirrorX != nullptr) { + lv_obj_set_state(ctx.switchMirrorX, LV_STATE_CHECKED, ctx.config.mirror_x); + } + if (ctx.switchMirrorY != nullptr) { + lv_obj_set_state(ctx.switchMirrorY, LV_STATE_CHECKED, ctx.config.mirror_y); + } + if (ctx.switchInvert != nullptr) { + lv_obj_set_state(ctx.switchInvert, LV_STATE_CHECKED, ctx.config.invert_color); + } + if (ctx.switchBgr != nullptr) { + lv_obj_set_state(ctx.switchBgr, LV_STATE_CHECKED, ctx.config.bgr_order); + } + + refreshOrientationChips(ctx); + refreshClockChips(ctx); + refreshHostChips(ctx); + + for (size_t i = 0; i < NUMERIC_FIELD_COUNT; i++) { + updateValueLabel(ctx, static_cast(i)); + } + + refreshResolutionChips(ctx); +} + +void setActionStatus(Context& ctx, const char* text) { + if (ctx.actionStatusLabel != nullptr) { + lv_label_set_text(ctx.actionStatusLabel, text); + } +} + +/** Updates the active/inactive status label. Call with the LVGL lock held. */ +void refreshStatus(Context& ctx) { + if (ctx.statusLabel == nullptr) { + return; + } + if (!ctx.moduleAvailable) { + lv_label_set_text(ctx.statusLabel, "Stopped"); + return; + } + + if (ctx.stoppedHint != nullptr) { + lv_obj_add_flag(ctx.stoppedHint, LV_OBJ_FLAG_HIDDEN); + } + + if (!ctx.api.isActive()) { + lv_label_set_text(ctx.statusLabel, "Module running, panel inactive"); + return; + } + + struct Device* device = nullptr; + if (ctx.api.getDevice(&device) != ERROR_NONE) { + lv_label_set_text(ctx.statusLabel, "Active"); + return; + } + + lv_label_set_text_fmt( + ctx.statusLabel, + "Active - %ux%u", + static_cast(display_get_resolution_x(device)), + static_cast(display_get_resolution_y(device)) + ); + device_put(device); +} + +/** The pickers and chips write ctx.config directly; only reject values that no control produces. */ +bool validateConfig(Context& ctx) { + if (ctx.config.width < 1 || ctx.config.width > 1024 || ctx.config.height < 1 || ctx.config.height > 1024) { + setActionStatus(ctx, "Width/height must be 1-1024"); + return false; + } + if (ctx.config.max_transfer_bytes < 64 || ctx.config.max_transfer_bytes > 65535) { + setActionStatus(ctx, "Max transfer must be 64-65535 bytes"); + return false; + } + return true; +} + +// endregion + +// region UI events + +void onClose(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + app_event_emit_close(ctx->appInstanceId); +} + +void onOrientationPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + auto* chip = static_cast(lv_event_get_current_target_obj(event)); + int index = -1; + for (int i = 0; i < 4; i++) { + if (ctx->orientationChips[i] == chip) { + index = i; + applyOrientation(ctx->config, i); + break; + } + } + if (index >= 0) { + // The presets are tied to the panel's two logical sizes, so portrait also swaps them. + const bool landscape = index == 0 || index == 2; + ctx->config.width = landscape ? 320 : 240; + ctx->config.height = landscape ? 240 : 320; + updateValueLabel(*ctx, NumericField::Width); + updateValueLabel(*ctx, NumericField::Height); + refreshResolutionChips(*ctx); + } + refreshOrientationChips(*ctx); + if (ctx->switchMirrorX != nullptr) { + lv_obj_set_state(ctx->switchMirrorX, LV_STATE_CHECKED, ctx->config.mirror_x); + } + if (ctx->switchMirrorY != nullptr) { + lv_obj_set_state(ctx->switchMirrorY, LV_STATE_CHECKED, ctx->config.mirror_y); + } +} + +void onClockPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + auto* chip = static_cast(lv_event_get_current_target_obj(event)); + for (uint32_t i = 0; i < CLOCK_PRESET_COUNT; i++) { + if (ctx->clockChips[i] == chip) { + ctx->config.pixel_clock_hz = CLOCK_PRESETS[i] * 1000 * 1000; + break; + } + } + refreshClockChips(*ctx); +} + +void onHostPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + auto* chip = static_cast(lv_event_get_current_target_obj(event)); + for (uint32_t i = 0; i < HOST_PRESET_COUNT; i++) { + if (ctx->hostChips[i] == chip) { + ctx->config.spi_host = static_cast(HOST_PRESETS[i]); + break; + } + } + refreshHostChips(*ctx); +} + +void onResolutionPresetPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + auto* chip = static_cast(lv_event_get_current_target_obj(event)); + const bool landscape = chip == ctx->resolutionChips[0]; + // Match the orientation too, so a size preset can't leave the panel with a mismatched swap_xy. + applyOrientation(ctx->config, landscape ? 0 : 1); + ctx->config.width = landscape ? 320 : 240; + ctx->config.height = landscape ? 240 : 320; + updateValueLabel(*ctx, NumericField::Width); + updateValueLabel(*ctx, NumericField::Height); + refreshOrientationChips(*ctx); + refreshResolutionChips(*ctx); + if (ctx->switchMirrorX != nullptr) { + lv_obj_set_state(ctx->switchMirrorX, LV_STATE_CHECKED, ctx->config.mirror_x); + } + if (ctx->switchMirrorY != nullptr) { + lv_obj_set_state(ctx->switchMirrorY, LV_STATE_CHECKED, ctx->config.mirror_y); + } +} + +// region Number dialog + +const char* dialogTitle(NumericField field) { + switch (field) { + case NumericField::Width: + return "Width"; + case NumericField::Height: + return "Height"; + case NumericField::MaxTransfer: + return "Max transfer"; + case NumericField::PinSck: + return "SCK"; + case NumericField::PinMosi: + return "MOSI"; + case NumericField::PinMiso: + return "MISO"; + case NumericField::PinDc: + return "DC"; + case NumericField::PinReset: + return "RESET"; + case NumericField::PinCs: + return "CS"; + default: + return ""; + } +} + +const char* dialogMessage(NumericField field) { + switch (field) { + case NumericField::Width: + case NumericField::Height: + return "1-1024 pixels"; + case NumericField::MaxTransfer: + return "1-64 KB"; + case NumericField::PinReset: + return "0-54, or empty for None"; + default: + return "0-54"; + } +} + +/** Digits only, surrounding spaces ignored. */ +bool parseUnsigned(const char* text, int32_t* out) { + if (text == nullptr) { + return false; + } + while (*text == ' ') { + text++; + } + int32_t value = 0; + bool any = false; + for (; *text != '\0' && *text != ' '; text++) { + if (*text < '0' || *text > '9') { + return false; + } + value = value * 10 + (*text - '0'); + any = true; + if (value > 100000) { + return false; + } + } + while (*text == ' ') { + text++; + } + if (!any || *text != '\0') { + return false; + } + *out = value; + return true; +} + +int32_t clampField(NumericField field, int32_t value) { + int32_t minValue = 0; + int32_t maxValue = 54; + if (field == NumericField::Width || field == NumericField::Height) { + minValue = 1; + maxValue = 1024; + } else if (field == NumericField::MaxTransfer) { + minValue = 1; + maxValue = 64; + } + if (value < minValue) { + value = minValue; + } + if (value > maxValue) { + value = maxValue; + } + return value; +} + +void openNumberDialog(Context& ctx, NumericField field) { + if (ctx.dialog.instanceId != 0) { + return; + } + + char prefilled[16] = {}; + const int32_t current = getNumericField(ctx, field); + if (current >= 0) { + std::snprintf(prefilled, sizeof(prefilled), "%d", static_cast(current)); + } + + const char* argv[] = { dialogTitle(field), dialogMessage(field), prefilled }; + + AppStartContext start {}; + error_t result = app_start_context_from_id(INPUT_DIALOG_APP_ID, &start); + if (result == ERROR_NONE) { + AppStreamBinding binding = { + .producer_fd = STDOUT_FILENO, + .stream = &ctx.dialog.stream, + .buffer = ctx.dialog.buffer, + .buffer_capacity = sizeof(ctx.dialog.buffer), + .event_group = ctx.eventGroup, + .window_size = {}, + .alias_fd = -1, + }; + app_start_context_set_arguments_ext(&start, 3, argv); + app_start_context_set_streams(&start, &binding, 1); + app_start_context_set_parent(&start, ctx.appInstanceId); + + AppInstanceId instanceId = 0; + result = app_start_with_context(&start, &instanceId); + if (result == ERROR_NONE) { + ctx.dialog.field = field; + ctx.dialog.instanceId = instanceId; + return; + } + } + + lvgl_lock(); + setActionStatus(ctx, error_to_string(result)); + lvgl_unlock(); +} + +/** Runs on the app task when APP_EVENT_RESULT for the number dialog arrives. */ +void handleNumberDialogResult(Context& ctx, AppInstanceId launchId, int32_t resultCode) { + const NumericField field = ctx.dialog.field; + ctx.dialog.instanceId = 0; + + if (resultCode == 0) { + char text[sizeof(ctx.dialog.buffer)] = {}; + const size_t length = app_stream_read(&ctx.dialog.stream, text, sizeof(text) - 1); + text[length] = '\0'; + + int32_t value = 0; + if (length == 0 && field == NumericField::PinReset) { + value = -1; + } else if (!parseUnsigned(text, &value)) { + lvgl_lock(); + setActionStatus(ctx, "Enter a whole number"); + lvgl_unlock(); + app_stream_unsubscribe(&ctx.dialog.stream); + app_manager_stop(launchId); + return; + } else { + value = clampField(field, value); + } + + lvgl_lock(); + setNumericField(ctx, field, value); + updateValueLabel(ctx, field); + refreshResolutionChips(ctx); + lvgl_unlock(); + } + + app_stream_unsubscribe(&ctx.dialog.stream); + app_manager_stop(launchId); +} + +void onValueRowPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + auto* button = static_cast(lv_event_get_current_target_obj(event)); + const auto field = static_cast(reinterpret_cast(lv_obj_get_user_data(button))); + ctx->editField = static_cast(field); + ctx->request = REQUEST_EDIT_NUMBER; + task_event_group_signal(ctx->eventGroup, ctx->requestBit); +} + +// endregion + +void onEnableChanged(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->enableTarget = lv_obj_has_state(ctx->switchEnable, LV_STATE_CHECKED); + ctx->request = REQUEST_SET_ENABLED; + task_event_group_signal(ctx->eventGroup, ctx->requestBit); +} + +void onSavePressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->request = REQUEST_SAVE; + task_event_group_signal(ctx->eventGroup, ctx->requestBit); +} + +void onApplyPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + setActionStatus(*ctx, "Applying..."); + ctx->request = REQUEST_APPLY; + task_event_group_signal(ctx->eventGroup, ctx->requestBit); +} + +void onRevertPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->request = REQUEST_REVERT; + task_event_group_signal(ctx->eventGroup, ctx->requestBit); +} + +// endregion + +// region Window + +void buildUi(lv_obj_t* root, Context& ctx) { + // The module may have been started from Kernel settings while this window was hidden; the + // window manager rebuilds the widgets when the app comes back, so re-check here. + if (!ctx.moduleAvailable && acquireApi(ctx.api)) { + ctx.moduleAvailable = true; + if (ctx.api.load(&ctx.config) != ERROR_NONE) { + ctx.api.defaults(&ctx.config); + } + ctx.config.enabled = true; + ctx.request = REQUEST_REFRESH; + task_event_group_signal(ctx.eventGroup, ctx.requestBit); + } + + const bool compact = lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT; + const lv_coord_t pad = compact ? 8 : 12; + const lv_coord_t gap = compact ? 4 : 8; + const lv_coord_t chipMinWidth = compact ? 48 : 56; + + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_scrollable(root, false); + + auto* toolbar = lvgl_toolbar_create(root, "ILI9341 Panel"); + lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClose, &ctx); + + auto* content = lv_obj_create(root); + lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(content, LV_PCT(100)); + lv_obj_set_flex_grow(content, 1); + lv_obj_set_style_pad_all(content, pad, LV_STATE_DEFAULT); + lv_obj_set_style_pad_gap(content, gap, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(content, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_scroll_dir(content, LV_DIR_VER); + + // Status card + auto* statusCard = createCard(content, gap); + ctx.statusLabel = lv_label_create(statusCard); + lv_obj_set_width(ctx.statusLabel, LV_PCT(100)); + lv_obj_set_style_text_font(ctx.statusLabel, lvgl_get_text_font(FONT_SIZE_LARGE), LV_STATE_DEFAULT); + + ctx.actionStatusLabel = lv_label_create(statusCard); + lv_obj_set_width(ctx.actionStatusLabel, LV_PCT(100)); + lv_obj_set_style_text_color(ctx.actionStatusLabel, lv_palette_darken(LV_PALETTE_GREY, 3), LV_STATE_DEFAULT); + lv_label_set_text(ctx.actionStatusLabel, ctx.moduleAvailable ? "Ready" : "Panel is stopped"); + + // The switch mirrors the module's started state, so it matches Kernel settings. + ctx.switchEnable = createSwitchRow(statusCard, "Enabled", ctx.moduleAvailable, gap); + lv_obj_add_event_cb(ctx.switchEnable, onEnableChanged, LV_EVENT_VALUE_CHANGED, &ctx); + + if (!ctx.moduleAvailable) { + auto* hint = lv_label_create(statusCard); + lv_obj_set_width(hint, LV_PCT(100)); + lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP); + lv_label_set_text( + hint, + "The panel module is stopped; these are built-in defaults. Turn the switch on to use the " + "panel and load its saved settings." + ); + ctx.stoppedHint = hint; + } + + // Panel card + auto* panelCard = createCard(content, gap); + createSectionTitle(panelCard, "Panel"); + + auto* orientationTitle = lv_label_create(panelCard); + lv_label_set_text(orientationTitle, "Orientation"); + + auto* orientationRow = createRow(panelCard, gap); + constexpr const char* ORIENTATION_LABELS[] = { "Landscape", "Portrait", "Landscape 180", "Portrait 180" }; + for (int i = 0; i < 4; i++) { + auto* chip = createChip(orientationRow, ORIENTATION_LABELS[i], chipMinWidth); + lv_obj_add_event_cb(chip, onOrientationPressed, LV_EVENT_SHORT_CLICKED, &ctx); + ctx.orientationChips[i] = chip; + } + + ctx.switchMirrorX = createSwitchRow(panelCard, "Mirror X", ctx.config.mirror_x, gap); + lv_obj_add_event_cb(ctx.switchMirrorX, [](lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->config.mirror_x = lv_obj_has_state(ctx->switchMirrorX, LV_STATE_CHECKED); + refreshOrientationChips(*ctx); + }, LV_EVENT_VALUE_CHANGED, &ctx); + + ctx.switchMirrorY = createSwitchRow(panelCard, "Mirror Y", ctx.config.mirror_y, gap); + lv_obj_add_event_cb(ctx.switchMirrorY, [](lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->config.mirror_y = lv_obj_has_state(ctx->switchMirrorY, LV_STATE_CHECKED); + refreshOrientationChips(*ctx); + }, LV_EVENT_VALUE_CHANGED, &ctx); + + ctx.switchInvert = createSwitchRow(panelCard, "Invert colours", ctx.config.invert_color, gap); + lv_obj_add_event_cb(ctx.switchInvert, [](lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->config.invert_color = lv_obj_has_state(ctx->switchInvert, LV_STATE_CHECKED); + }, LV_EVENT_VALUE_CHANGED, &ctx); + + ctx.switchBgr = createSwitchRow(panelCard, "BGR order", ctx.config.bgr_order, gap); + lv_obj_add_event_cb(ctx.switchBgr, [](lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->config.bgr_order = lv_obj_has_state(ctx->switchBgr, LV_STATE_CHECKED); + }, LV_EVENT_VALUE_CHANGED, &ctx); + + // Size and bus + auto* sizeCard = createCard(content, gap); + createSectionTitle(sizeCard, "Size and bus"); + + auto* resolutionRow = createRow(sizeCard, gap); + auto* presetLandscape = createChip(resolutionRow, "320x240", chipMinWidth); + auto* presetPortrait = createChip(resolutionRow, "240x320", chipMinWidth); + ctx.resolutionChips[0] = presetLandscape; + ctx.resolutionChips[1] = presetPortrait; + lv_obj_add_event_cb(presetLandscape, onResolutionPresetPressed, LV_EVENT_SHORT_CLICKED, &ctx); + lv_obj_add_event_cb(presetPortrait, onResolutionPresetPressed, LV_EVENT_SHORT_CLICKED, &ctx); + + ctx.valueLabels[static_cast(NumericField::Width)] = createValueRow(sizeCard, ctx, "Width", NumericField::Width, gap); + ctx.valueLabels[static_cast(NumericField::Height)] = createValueRow(sizeCard, ctx, "Height", NumericField::Height, gap); + + auto* hostTitle = lv_label_create(sizeCard); + lv_label_set_text(hostTitle, "SPI host"); + auto* hostRow = createRow(sizeCard, gap); + constexpr const char* HOST_LABELS[] = { "SPI2", "SPI3" }; + for (uint32_t i = 0; i < HOST_PRESET_COUNT; i++) { + auto* chip = createChip(hostRow, HOST_LABELS[i], chipMinWidth); + lv_obj_add_event_cb(chip, onHostPressed, LV_EVENT_SHORT_CLICKED, &ctx); + ctx.hostChips[i] = chip; + } + + auto* clockTitle = lv_label_create(sizeCard); + lv_label_set_text(clockTitle, "SPI clock"); + auto* clockRow = createRow(sizeCard, gap); + constexpr const char* CLOCK_LABELS[] = { "10 MHz", "20 MHz", "40 MHz", "80 MHz" }; + for (uint32_t i = 0; i < CLOCK_PRESET_COUNT; i++) { + auto* chip = createChip(clockRow, CLOCK_LABELS[i], chipMinWidth); + lv_obj_add_event_cb(chip, onClockPressed, LV_EVENT_SHORT_CLICKED, &ctx); + ctx.clockChips[i] = chip; + } + + ctx.valueLabels[static_cast(NumericField::MaxTransfer)] = + createValueRow(sizeCard, ctx, "Max transfer", NumericField::MaxTransfer, gap); + + // Pins + auto* pinsCard = createCard(content, gap); + createSectionTitle(pinsCard, "Pins"); + + ctx.valueLabels[static_cast(NumericField::PinSck)] = createValueRow(pinsCard, ctx, "SCK", NumericField::PinSck, gap); + ctx.valueLabels[static_cast(NumericField::PinMosi)] = createValueRow(pinsCard, ctx, "MOSI", NumericField::PinMosi, gap); + ctx.valueLabels[static_cast(NumericField::PinMiso)] = createValueRow(pinsCard, ctx, "MISO", NumericField::PinMiso, gap); + ctx.valueLabels[static_cast(NumericField::PinDc)] = createValueRow(pinsCard, ctx, "DC", NumericField::PinDc, gap); + ctx.valueLabels[static_cast(NumericField::PinReset)] = + createValueRow(pinsCard, ctx, "RESET (None = unused)", NumericField::PinReset, gap); + ctx.valueLabels[static_cast(NumericField::PinCs)] = createValueRow(pinsCard, ctx, "CS", NumericField::PinCs, gap); + + // Actions + auto* actionsRow = createRow(content, gap); + auto* saveChip = createChip(actionsRow, "Save", chipMinWidth); + lv_obj_add_event_cb(saveChip, onSavePressed, LV_EVENT_SHORT_CLICKED, &ctx); + auto* applyChip = createChip(actionsRow, "Apply", chipMinWidth); + lv_obj_add_event_cb(applyChip, onApplyPressed, LV_EVENT_SHORT_CLICKED, &ctx); + auto* revertChip = createChip(actionsRow, "Revert", chipMinWidth); + lv_obj_add_event_cb(revertChip, onRevertPressed, LV_EVENT_SHORT_CLICKED, &ctx); + + writeConfigToWidgets(ctx); + refreshStatus(ctx); +} + +void createWidgets(lv_obj_t* root, void* userData) { + auto* ctx = static_cast(userData); + buildUi(root, *ctx); +} + +void destroyWidgets(void* userData) { + auto* ctx = static_cast(userData); + ctx->statusLabel = nullptr; + ctx->actionStatusLabel = nullptr; + ctx->stoppedHint = nullptr; + ctx->switchEnable = nullptr; + ctx->switchMirrorX = nullptr; + ctx->switchMirrorY = nullptr; + ctx->switchInvert = nullptr; + ctx->switchBgr = nullptr; + for (auto*& chip : ctx->orientationChips) { + chip = nullptr; + } + for (auto*& chip : ctx->resolutionChips) { + chip = nullptr; + } + for (auto*& chip : ctx->clockChips) { + chip = nullptr; + } + for (auto*& chip : ctx->hostChips) { + chip = nullptr; + } + for (auto*& label : ctx->valueLabels) { + label = nullptr; + } +} + +// endregion + +// region Request handling (app task, LVGL lock not held) + +/** Starts or stops the panel module, so this switch and Kernel settings always agree. */ +void handleSetEnabled(Context& ctx, bool enable) { + if (enable == ctx.moduleAvailable) { + lvgl_lock(); + if (ctx.switchEnable != nullptr) { + lv_obj_set_state(ctx.switchEnable, LV_STATE_CHECKED, ctx.moduleAvailable); + } + lvgl_unlock(); + return; + } + + error_t result = ERROR_NONE; + + if (enable) { + result = app_module_set_started(PACKAGE_ID, MODULE_NAME, true); + if (result == ERROR_NONE) { + ctx.moduleAvailable = acquireApi(ctx.api); + if (!ctx.moduleAvailable) { + // Without its API the app can't do anything with the module, so don't leave it running. + result = ERROR_NOT_FOUND; + app_module_set_started(PACKAGE_ID, MODULE_NAME, false); + } + } + if (result == ERROR_NONE) { + Ili9341PanelConfig loaded {}; + if (ctx.api.load(&loaded) == ERROR_NONE) { + if (!loaded.enabled) { + // The module was saved as disabled by an older build; the switch owns that state now. + loaded.enabled = true; + if (ctx.api.save(&loaded) == ERROR_NONE) { + ctx.api.apply(); + } + } + ctx.config = loaded; + ctx.config.enabled = true; + } else { + ctx.api.defaults(&ctx.config); + ctx.config.enabled = true; + } + if (ctx.api.isActive()) { + const error_t attachResult = attachPanelDisplay(ctx); + if (attachResult != ERROR_NONE) { + result = attachResult; + } + } + } + } else { + detachPanelDisplay(ctx, true); + releaseApi(ctx.api); + ctx.moduleAvailable = false; + result = app_module_set_started(PACKAGE_ID, MODULE_NAME, false); + if (result != ERROR_NONE) { + // The module is still running (in use elsewhere); reflect that again. + ctx.moduleAvailable = acquireApi(ctx.api); + if (ctx.moduleAvailable && ctx.api.isActive()) { + attachPanelDisplay(ctx); + } + } + } + + lvgl_lock(); + ctx.enableTarget = ctx.moduleAvailable; + if (ctx.switchEnable != nullptr) { + lv_obj_set_state(ctx.switchEnable, LV_STATE_CHECKED, ctx.moduleAvailable); + } + if (ctx.moduleAvailable) { + writeConfigToWidgets(ctx); + } + refreshStatus(ctx); + if (result == ERROR_NONE) { + setActionStatus(ctx, enable ? "Started" : "Stopped"); + } else { + char buffer[64]; + std::snprintf( + buffer, + sizeof(buffer), + "%s: %s", + enable ? "Start failed" : "Stop failed", + error_to_string(result) + ); + setActionStatus(ctx, buffer); + } + lvgl_unlock(); +} + +void handleSave(Context& ctx, const Ili9341PanelConfig& config) { + error_t result = ctx.api.save(&config); + lvgl_lock(); + if (result == ERROR_NONE) { + ctx.config = config; + writeConfigToWidgets(ctx); + setActionStatus(ctx, "Saved"); + } else { + setActionStatus(ctx, error_to_string(result)); + } + lvgl_unlock(); +} + +void handleApply(Context& ctx, const Ili9341PanelConfig& config) { + // Kept so a configuration that fails to start can't leave the panel stopped until the next save. + Ili9341PanelConfig previous {}; + const bool hasPrevious = ctx.api.load(&previous) == ERROR_NONE; + + detachPanelDisplay(ctx); + + error_t result = ctx.api.save(&config); + if (result == ERROR_NONE) { + result = ctx.api.apply(); + } + if (result != ERROR_NONE && hasPrevious && ctx.api.save(&previous) == ERROR_NONE) { + ctx.api.apply(); + } + if (ctx.api.isActive()) { + const error_t attachResult = attachPanelDisplay(ctx); + if (result == ERROR_NONE) { + result = attachResult; + } + } + + lvgl_lock(); + if (result == ERROR_NONE) { + ctx.config = config; + } else if (ctx.api.load(&ctx.config) != ERROR_NONE) { + // Keep the attempted values in the form so they can be corrected. + } + ctx.config.enabled = true; + writeConfigToWidgets(ctx); + if (result == ERROR_NONE) { + setActionStatus(ctx, "Applied"); + } else { + char buffer[64]; + std::snprintf(buffer, sizeof(buffer), "Apply failed: %s", error_to_string(result)); + setActionStatus(ctx, buffer); + } + refreshStatus(ctx); + lvgl_unlock(); +} + +void handleRevert(Context& ctx) { + lvgl_lock(); + error_t result = ctx.api.load(&ctx.config); + if (result == ERROR_NONE) { + ctx.config.enabled = true; + writeConfigToWidgets(ctx); + setActionStatus(ctx, "Reverted to saved settings"); + } else { + setActionStatus(ctx, error_to_string(result)); + } + lvgl_unlock(); +} + +void handleRequest(Context& ctx, int32_t request) { + if (request == REQUEST_SET_ENABLED) { + handleSetEnabled(ctx, ctx.enableTarget.load()); + return; + } + + if (request == REQUEST_EDIT_NUMBER) { + const int32_t field = ctx.editField.exchange(-1); + if (field >= 0) { + openNumberDialog(ctx, static_cast(field)); + } + return; + } + + if (request == REQUEST_REFRESH) { + // buildUi() found the module running after it had been started elsewhere. + if (!ctx.api.isActive() && !ctx.config.enabled) { + ctx.config.enabled = true; + if (ctx.api.save(&ctx.config) == ERROR_NONE) { + ctx.api.apply(); + } + } + if (ctx.api.isActive()) { + attachPanelDisplay(ctx); + } + lvgl_lock(); + refreshStatus(ctx); + lvgl_unlock(); + return; + } + + if (request == REQUEST_REVERT) { + if (!ctx.moduleAvailable) { + lvgl_lock(); + setActionStatus(ctx, "Enable the panel first"); + lvgl_unlock(); + return; + } + handleRevert(ctx); + return; + } + + lvgl_lock(); + const bool valid = validateConfig(ctx); + const Ili9341PanelConfig config = ctx.config; + lvgl_unlock(); + if (!valid) { + return; + } + + // The switch is the only thing that turns the panel on; these actions just fail while it's off. + if (!ctx.moduleAvailable) { + lvgl_lock(); + setActionStatus(ctx, "Enable the panel first"); + lvgl_unlock(); + return; + } + + if (request == REQUEST_SAVE) { + handleSave(ctx, config); + } else if (request == REQUEST_APPLY) { + handleApply(ctx, config); + } +} + +// endregion + +} // namespace + +extern "C" int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + + AppInstanceId app_instance_id = app_scheduler_current_app_id(); + + Context ctx; + ctx.appInstanceId = app_instance_id; + ctx.moduleAvailable = acquireApi(ctx.api); + if (ctx.moduleAvailable) { + error_t result = ctx.api.load(&ctx.config); + if (result != ERROR_NONE) { + ctx.api.defaults(&ctx.config); + } + } else { + setFallbackDefaults(ctx.config); + } + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + ctx.eventGroup = &event_group; + check(task_event_group_claim_bit(&event_group, &ctx.requestBit) == ERROR_NONE); + + struct AppEventSubscription subscription {}; + check(app_event_subscribe(&subscription, &event_group) == ERROR_NONE); + + WindowId window = window_manager_create_ext(app_instance_id, createWidgets, destroyWidgets, &ctx); + + bool shouldClose = false; + while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + const int32_t request = ctx.request.exchange(REQUEST_NONE); + if (request != REQUEST_NONE) { + handleRequest(ctx, request); + } + + struct AppEvent event {}; + while (app_event_poll(&subscription, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } + if (event.type == APP_EVENT_RESULT && event.result.launch_id == ctx.dialog.instanceId) { + handleNumberDialogResult(ctx, event.result.launch_id, event.result.result); + } + } + } + + window_manager_remove(window); + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_release_bit(&event_group, ctx.requestBit); + task_event_group_destruct(&event_group); + releaseApi(ctx.api); + + return 0; +} diff --git a/Apps/MystifySpiDemo/ili9341panel/CMakeLists.txt b/Apps/MystifySpiDemo/ili9341panel/CMakeLists.txt new file mode 100644 index 0000000..105e094 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341panel/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(tactility.mystifyspidemo.ili9341panel) +project(tactility.mystifyspidemo.ili9341panel) +tactility_project_post(tactility.mystifyspidemo.ili9341panel) diff --git a/Apps/MystifySpiDemo/ili9341panel/main/CMakeLists.txt b/Apps/MystifySpiDemo/ili9341panel/main/CMakeLists.txt new file mode 100644 index 0000000..b26d03e --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341panel/main/CMakeLists.txt @@ -0,0 +1,8 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +tactility_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS ../../common +) diff --git a/Apps/MystifySpiDemo/ili9341panel/main/Source/main.cpp b/Apps/MystifySpiDemo/ili9341panel/main/Source/main.cpp new file mode 100644 index 0000000..5582bca --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341panel/main/Source/main.cpp @@ -0,0 +1,475 @@ +// SPDX-License-Identifier: Apache-2.0 +/** + * Headless terminal wrapper around the ILI9341 panel module's settings API. + * The shell exposes this binary as the "ili9341panel" command. + */ +#include + +#include "panel_display.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr const char* PACKAGE_ID = "tactility.mystifyspidemo"; +constexpr const char* MODULE_NAME = "ili9341-panel"; + +struct PanelApi { + decltype(&ili9341panel_settings_defaults) defaults = nullptr; + decltype(&ili9341panel_settings_load) load = nullptr; + decltype(&ili9341panel_settings_save) save = nullptr; + decltype(&ili9341panel_apply) apply = nullptr; + decltype(&ili9341panel_is_active) isActive = nullptr; + decltype(&ili9341panel_get_device) getDevice = nullptr; + Module* module = nullptr; +}; + +bool acquireApi(PanelApi& api) { + Module* module = nullptr; + + auto resolve = [&module](const char* name, auto& function) { + uintptr_t address = 0; + Module* owner = nullptr; + if (!module_resolve_symbol_global_get(name, &address, &owner)) { + return false; + } + function = reinterpret_cast>(address); + if (module == nullptr) { + module = owner; + } else { + module_put(owner); + } + return true; + }; + + const bool found = resolve("ili9341panel_settings_defaults", api.defaults) && + resolve("ili9341panel_settings_load", api.load) && + resolve("ili9341panel_settings_save", api.save) && + resolve("ili9341panel_apply", api.apply) && + resolve("ili9341panel_is_active", api.isActive) && + resolve("ili9341panel_get_device", api.getDevice); + + // Symbol lookup also finds stopped modules, so check the state as well. + if (!found || !module_is_started(module)) { + if (module != nullptr) { + module_put(module); + } + api = PanelApi {}; + return false; + } + + api.module = module; + return true; +} + +void releaseApi(PanelApi& api) { + if (api.module != nullptr) { + module_put(api.module); + } + api = PanelApi {}; +} + +int orientationIndex(const Ili9341PanelConfig& config) { + if (config.swap_xy && !config.mirror_x && !config.mirror_y) return 0; // landscape + if (!config.swap_xy && !config.mirror_x && config.mirror_y) return 1; // portrait + if (config.swap_xy && config.mirror_x && config.mirror_y) return 2; // landscape 180 + if (!config.swap_xy && config.mirror_x && !config.mirror_y) return 3; // portrait 180 + return -1; +} + +const char* orientationName(int index) { + switch (index) { + case 0: + return "landscape"; + case 1: + return "portrait"; + case 2: + return "landscape180"; + case 3: + return "portrait180"; + default: + return "custom"; + } +} + +bool applyOrientation(Ili9341PanelConfig& config, const char* name) { + if (strcmp(name, "landscape") == 0) { + config.swap_xy = true; + config.mirror_x = false; + config.mirror_y = false; + config.width = 320; + config.height = 240; + } else if (strcmp(name, "portrait") == 0) { + config.swap_xy = false; + config.mirror_x = false; + config.mirror_y = true; + config.width = 240; + config.height = 320; + } else if (strcmp(name, "landscape180") == 0) { + config.swap_xy = true; + config.mirror_x = true; + config.mirror_y = true; + config.width = 320; + config.height = 240; + } else if (strcmp(name, "portrait180") == 0) { + config.swap_xy = false; + config.mirror_x = true; + config.mirror_y = false; + config.width = 240; + config.height = 320; + } else { + return false; + } + return true; +} + +bool parseOnOff(const char* text, bool* out) { + if (strcmp(text, "on") == 0 || strcmp(text, "true") == 0 || strcmp(text, "1") == 0) { + *out = true; + return true; + } + if (strcmp(text, "off") == 0 || strcmp(text, "false") == 0 || strcmp(text, "0") == 0) { + *out = false; + return true; + } + return false; +} + +bool parseNumber(const char* text, long min, long max, long* out) { + char* end = nullptr; + const long value = std::strtol(text, &end, 10); + if (end == text || *end != '\0' || value < min || value > max) { + return false; + } + *out = value; + return true; +} + +const char* yesNo(bool value) { + return value ? "on" : "off"; +} + +void printUsage() { + std::printf( + "Usage: ili9341panel [arguments]\n" + "\n" + " status Module and panel state\n" + " get Print all settings\n" + " on | off Start/stop the panel module\n" + " apply Re-create the panel from the saved settings\n" + " set Change and save a setting\n" + "\n" + "keys:\n" + " orientation landscape|portrait|landscape180|portrait180\n" + " swapxy|mirrorx|mirrory|invert|bgr on|off\n" + " width|height <1-1024> clock <10|20|40|80> (MHz)\n" + " host <2|3> maxkb <1-64>\n" + " sck|mosi|miso|dc|cs <0-54> reset <0-54|none>\n" + ); +} + +int commandStatus(PanelApi& api) { + struct Device* device = nullptr; + const bool active = api.getDevice(&device) == ERROR_NONE && device != nullptr; + + Ili9341PanelConfig config {}; + if (api.load(&config) != ERROR_NONE) { + std::printf("settings: could not be loaded\n"); + return 1; + } + + std::printf("module: started\n"); + if (active) { + std::printf( + "panel: active, %ux%u, color format %d\n", + static_cast(display_get_resolution_x(device)), + static_cast(display_get_resolution_y(device)), + static_cast(display_get_color_format(device)) + ); + device_put(device); + } else { + std::printf("panel: inactive\n"); + } + + std::printf("spi: SPI%d at %u MHz\n", static_cast(config.spi_host) + 1, + static_cast(config.pixel_clock_hz / 1000000)); + std::printf( + "pins: sck=%d mosi=%d miso=%d dc=%d reset=%d cs=%d\n", + static_cast(config.pin_sck), + static_cast(config.pin_mosi), + static_cast(config.pin_miso), + static_cast(config.pin_dc), + static_cast(config.pin_reset), + static_cast(config.pin_cs) + ); + std::printf( + "display: %ux%u, orientation %s, invert %s, bgr %s, max %u KB\n", + static_cast(config.width), + static_cast(config.height), + orientationName(orientationIndex(config)), + yesNo(config.invert_color), + yesNo(config.bgr_order), + static_cast((config.max_transfer_bytes + 512) / 1024) + ); + return 0; +} + +int commandGet(PanelApi& api) { + Ili9341PanelConfig config {}; + const error_t result = api.load(&config); + if (result != ERROR_NONE) { + std::printf("error: %s\n", error_to_string(result)); + return 1; + } + + std::printf("enabled=%s\n", yesNo(config.enabled)); + std::printf("spi_host=%d\n", static_cast(config.spi_host)); + std::printf("sck=%d\n", static_cast(config.pin_sck)); + std::printf("mosi=%d\n", static_cast(config.pin_mosi)); + std::printf("miso=%d\n", static_cast(config.pin_miso)); + std::printf("dc=%d\n", static_cast(config.pin_dc)); + std::printf("reset=%d\n", static_cast(config.pin_reset)); + std::printf("cs=%d\n", static_cast(config.pin_cs)); + std::printf("width=%u\n", static_cast(config.width)); + std::printf("height=%u\n", static_cast(config.height)); + std::printf("swap_xy=%s\n", yesNo(config.swap_xy)); + std::printf("mirror_x=%s\n", yesNo(config.mirror_x)); + std::printf("mirror_y=%s\n", yesNo(config.mirror_y)); + std::printf("invert=%s\n", yesNo(config.invert_color)); + std::printf("bgr=%s\n", yesNo(config.bgr_order)); + std::printf("clock_mhz=%u\n", static_cast(config.pixel_clock_hz / 1000000)); + std::printf("max_transfer=%u\n", static_cast(config.max_transfer_bytes)); + return 0; +} + +int commandOn() { + error_t result = app_module_set_started(PACKAGE_ID, MODULE_NAME, true); + if (result != ERROR_NONE) { + std::printf("error: could not start the module: %s\n", error_to_string(result)); + return 1; + } + + PanelApi api {}; + if (!acquireApi(api)) { + std::printf("error: the module started but its settings API is unavailable\n"); + return 1; + } + + // The switch owns "enabled" in the apps; normalize a config saved as disabled by an older build. + Ili9341PanelConfig config {}; + if (api.load(&config) == ERROR_NONE && !config.enabled) { + config.enabled = true; + if (api.save(&config) == ERROR_NONE) { + api.apply(); + } + } + + struct Device* device = nullptr; + if (api.getDevice(&device) == ERROR_NONE && device != nullptr) { + if (ili9341panelui::findLvglDisplay(device) == nullptr) { + ili9341panelui::attachToLvgl(device); + } + device_put(device); + std::printf("panel started\n"); + } else { + std::printf("module started; panel inactive (check the saved settings)\n"); + } + + releaseApi(api); + return 0; +} + +int commandOff() { + PanelApi api {}; + if (acquireApi(api)) { + struct Device* device = nullptr; + if (api.getDevice(&device) == ERROR_NONE && device != nullptr) { + ili9341panelui::detachFromLvgl(device, true); + device_put(device); + } + releaseApi(api); + } + + const error_t result = app_module_set_started(PACKAGE_ID, MODULE_NAME, false); + if (result != ERROR_NONE) { + std::printf("error: could not stop the module: %s\n", error_to_string(result)); + return 1; + } + std::printf("panel stopped\n"); + return 0; +} + +int commandApply(PanelApi& api) { + // Apply works on the saved settings, so normalize "enabled" first (the CLI has no off state of + // its own; the module switch does). + Ili9341PanelConfig config {}; + if (api.load(&config) == ERROR_NONE && !config.enabled) { + config.enabled = true; + api.save(&config); + } + + struct Device* device = nullptr; + if (api.getDevice(&device) == ERROR_NONE && device != nullptr) { + ili9341panelui::detachFromLvgl(device); + device_put(device); + } + + error_t result = api.apply(); + if (result == ERROR_NONE && api.isActive()) { + if (api.getDevice(&device) == ERROR_NONE && device != nullptr) { + result = ili9341panelui::attachToLvgl(device); + device_put(device); + } + } + + if (result != ERROR_NONE) { + std::printf("error: apply failed: %s\n", error_to_string(result)); + return 1; + } + std::printf("settings applied\n"); + return 0; +} + +int commandSet(PanelApi& api, int argc, char* argv[]) { + if (argc < 4) { + printUsage(); + return 1; + } + + Ili9341PanelConfig config {}; + error_t result = api.load(&config); + if (result != ERROR_NONE) { + std::printf("error: %s\n", error_to_string(result)); + return 1; + } + + const char* key = argv[2]; + const char* value = argv[3]; + long number = 0; + bool flag = false; + + if (strcmp(key, "orientation") == 0) { + if (!applyOrientation(config, value)) { + std::printf("error: orientation must be landscape, portrait, landscape180 or portrait180\n"); + return 1; + } + } else if (strcmp(key, "swapxy") == 0 || strcmp(key, "mirrorx") == 0 || + strcmp(key, "mirrory") == 0 || strcmp(key, "invert") == 0 || strcmp(key, "bgr") == 0) { + if (!parseOnOff(value, &flag)) { + std::printf("error: %s must be on or off\n", key); + return 1; + } + if (strcmp(key, "swapxy") == 0) config.swap_xy = flag; + else if (strcmp(key, "mirrorx") == 0) config.mirror_x = flag; + else if (strcmp(key, "mirrory") == 0) config.mirror_y = flag; + else if (strcmp(key, "invert") == 0) config.invert_color = flag; + else config.bgr_order = flag; + } else if (strcmp(key, "width") == 0 || strcmp(key, "height") == 0) { + if (!parseNumber(value, 1, 1024, &number)) { + std::printf("error: %s must be 1-1024\n", key); + return 1; + } + if (strcmp(key, "width") == 0) config.width = static_cast(number); + else config.height = static_cast(number); + } else if (strcmp(key, "clock") == 0) { + if (!parseNumber(value, 10, 80, &number) || + (number != 10 && number != 20 && number != 40 && number != 80)) { + std::printf("error: clock must be 10, 20, 40 or 80\n"); + return 1; + } + config.pixel_clock_hz = static_cast(number) * 1000000; + } else if (strcmp(key, "host") == 0) { + if (!parseNumber(value, 1, 2, &number)) { + std::printf("error: host must be 1 (SPI2) or 2 (SPI3)\n"); + return 1; + } + config.spi_host = static_cast(number); + } else if (strcmp(key, "maxkb") == 0) { + if (!parseNumber(value, 1, 64, &number)) { + std::printf("error: maxkb must be 1-64\n"); + return 1; + } + const long bytes = number * 1024; + config.max_transfer_bytes = static_cast(bytes > 65535 ? 65535 : bytes); + } else if (strcmp(key, "sck") == 0 || strcmp(key, "mosi") == 0 || strcmp(key, "miso") == 0 || + strcmp(key, "dc") == 0 || strcmp(key, "cs") == 0 || strcmp(key, "reset") == 0) { + if (strcmp(key, "reset") == 0 && strcmp(value, "none") == 0) { + config.pin_reset = -1; + } else { + if (!parseNumber(value, 0, 54, &number)) { + std::printf("error: %s must be 0-54%s\n", key, strcmp(key, "reset") == 0 ? " or none" : ""); + return 1; + } + if (strcmp(key, "sck") == 0) config.pin_sck = static_cast(number); + else if (strcmp(key, "mosi") == 0) config.pin_mosi = static_cast(number); + else if (strcmp(key, "miso") == 0) config.pin_miso = static_cast(number); + else if (strcmp(key, "dc") == 0) config.pin_dc = static_cast(number); + else if (strcmp(key, "cs") == 0) config.pin_cs = static_cast(number); + else config.pin_reset = static_cast(number); + } + } else { + std::printf("error: unknown key '%s'\n", key); + printUsage(); + return 1; + } + + config.enabled = true; + result = api.save(&config); + if (result != ERROR_NONE) { + std::printf("error: could not save: %s\n", error_to_string(result)); + return 1; + } + + std::printf("%s saved; run 'ili9341panel apply' to use it\n", key); + return 0; +} + +} // namespace + +extern "C" int main(int argc, char* argv[]) { + if (argc < 2) { + printUsage(); + return 1; + } + + const char* command = argv[1]; + + if (strcmp(command, "on") == 0) { + return commandOn(); + } + if (strcmp(command, "off") == 0) { + return commandOff(); + } + + PanelApi api {}; + if (!acquireApi(api)) { + std::printf("error: the panel module is not running (use 'ili9341panel on')\n"); + return 1; + } + + int result = 1; + if (strcmp(command, "status") == 0) { + result = commandStatus(api); + } else if (strcmp(command, "get") == 0) { + result = commandGet(api); + } else if (strcmp(command, "apply") == 0) { + result = commandApply(api); + } else if (strcmp(command, "set") == 0) { + result = commandSet(api, argc, argv); + } else { + printUsage(); + } + + releaseApi(api); + return result; +} diff --git a/Apps/MystifySpiDemo/manifest.properties b/Apps/MystifySpiDemo/manifest.properties new file mode 100644 index 0000000..a20905d --- /dev/null +++ b/Apps/MystifySpiDemo/manifest.properties @@ -0,0 +1,23 @@ +manifest.version=0.3 +target.sdk=0.8.0-dev +target.platforms=esp32p4 +id=tactility.mystifyspidemo +version.name=0.5.0 +version.code=5 +name=Mystify SPI Demo +description=Adds a 2.8" ILI9341 SPI panel as a second display via an app-packaged driver module: Mystify demo, live settings, dashboard and terminal CLI. +app.0.id=tactility.mystifyspidemo +app.0.name=Mystify SPI Demo +app.0.binary=mystifyspidemo +app.1.id=tactility.ili9341panelsettings +app.1.name=ILI9341 Panel +app.1.binary=ili9341-panel-settings +app.2.id=tactility.paneldashboard +app.2.name=Panel Dashboard +app.2.binary=paneldashboard +app.3.id=tactility.ili9341panelcli +app.3.name=ILI9341 Panel CLI +app.3.binary=ili9341panel +app.3.headless=true +app.3.hidden=false +module.0.binary=ili9341-panel-module diff --git a/Apps/MystifySpiDemo/mystifyspidemo/CMakeLists.txt b/Apps/MystifySpiDemo/mystifyspidemo/CMakeLists.txt new file mode 100644 index 0000000..5f9805c --- /dev/null +++ b/Apps/MystifySpiDemo/mystifyspidemo/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(tactility.mystifyspidemo.mystifyspidemo) +project(tactility.mystifyspidemo.mystifyspidemo) +tactility_project_post(tactility.mystifyspidemo.mystifyspidemo) diff --git a/Apps/MystifySpiDemo/mystifyspidemo/main/CMakeLists.txt b/Apps/MystifySpiDemo/mystifyspidemo/main/CMakeLists.txt new file mode 100644 index 0000000..738e2d3 --- /dev/null +++ b/Apps/MystifySpiDemo/mystifyspidemo/main/CMakeLists.txt @@ -0,0 +1,16 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +file(GLOB_RECURSE GFX_FILES ../../../../Libraries/Gfx/Source/*.c*) +file(GLOB_RECURSE INPUT_FILES ../../../../Libraries/Input/Source/*.c*) + +tactility_component_register( + SRCS ${SOURCE_FILES} ${GFX_FILES} ${INPUT_FILES} + INCLUDE_DIRS + "Include" + ../../common + ../../../../Libraries/Gfx/Include + ../../../../Libraries/Input/Include + # The panel driver and SPI bus live in the module; the app only drives the device via Gfx. + REQUIRES graphics-module esp_driver_ppa log +) diff --git a/Apps/MystifySpiDemo/mystifyspidemo/main/Include/Application.h b/Apps/MystifySpiDemo/mystifyspidemo/main/Include/Application.h new file mode 100644 index 0000000..5e833cf --- /dev/null +++ b/Apps/MystifySpiDemo/mystifyspidemo/main/Include/Application.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +struct Device; + +/** + * Runs the SPI display demo until the user provides input or closes the app. + * + * @param display the primary display device, used to mirror the scene while LVGL is stopped + * @return nullptr on success, or a message to show in an alert dialog when the display could not + * be brought up. + */ +const char* runApplication(struct Device* display, AnyInput& input); diff --git a/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Application.cpp b/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Application.cpp new file mode 100644 index 0000000..2fbfb99 --- /dev/null +++ b/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Application.cpp @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "Application.h" + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +constexpr auto* TAG = "MystifyDemo"; + +namespace { + +// Tab5's panel is native portrait; direct rendering does not rotate it for us. A landscape +// logical frame keeps the mirrored scene upright. Use 270 if it appears upside down. +constexpr int PRIMARY_LANDSCAPE_ROTATION = 90; +// The mirrored scene is rendered at this logical size and scaled to the panel by the PPA. A +// smaller source is much cheaper to draw; 16:9 matches the Tab5 panel. +constexpr int PRIMARY_MIRROR_WIDTH = 640; +constexpr int PRIMARY_MIRROR_HEIGHT = 360; + +constexpr int TARGET_FPS = 30; +constexpr int64_t FRAME_BUDGET_US = 1'000'000 / TARGET_FPS; + +constexpr int WALKER_COUNT = 2; +constexpr int VERTEX_COUNT = 4; +constexpr int TRAIL_LENGTH = 6; +// Normalised: both the 320x240 panel and the primary frame use the same simulation. +constexpr float PLAY_MARGIN = 0.02f; + +uint16_t fade565(uint16_t rgb565, int brightness /* 0..255 */) { + const int r = ((rgb565 >> 11) & 0x1F) * brightness / 255; + const int g = ((rgb565 >> 5) & 0x3F) * brightness / 255; + const int b = (rgb565 & 0x1F) * brightness / 255; + return static_cast((r << 11) | (g << 5) | b); +} + +float randomFloat(float minimum, float maximum) { + const uint32_t step = esp_random() % 1000; + return minimum + (maximum - minimum) * (static_cast(step) / 999.0f); +} + +struct Walker { + // Normalised positions in [0, 1]; velocities in screen-heights per second. + float x[VERTEX_COUNT]; + float y[VERTEX_COUNT]; + float velocityX[VERTEX_COUNT]; + float velocityY[VERTEX_COUNT]; + uint16_t color = 0xFFFF; + // history[0] is the current frame, history[TRAIL_LENGTH - 1] the oldest echo. + float historyX[TRAIL_LENGTH][VERTEX_COUNT]; + float historyY[TRAIL_LENGTH][VERTEX_COUNT]; +}; + +void resetWalker(Walker& walker, uint16_t color) { + walker.color = color; + for (int vertex = 0; vertex < VERTEX_COUNT; vertex++) { + walker.x[vertex] = randomFloat(PLAY_MARGIN, 1.0f - PLAY_MARGIN); + walker.y[vertex] = randomFloat(PLAY_MARGIN, 1.0f - PLAY_MARGIN); + walker.velocityX[vertex] = randomFloat(0.08f, 0.22f) * ((esp_random() & 1) != 0 ? 1.0f : -1.0f); + walker.velocityY[vertex] = randomFloat(0.08f, 0.22f) * ((esp_random() & 1) != 0 ? 1.0f : -1.0f); + } + for (int age = 0; age < TRAIL_LENGTH; age++) { + std::memcpy(walker.historyX[age], walker.x, sizeof(walker.x)); + std::memcpy(walker.historyY[age], walker.y, sizeof(walker.y)); + } +} + +void moveWalker(Walker& walker, float deltaSeconds) { + for (int vertex = 0; vertex < VERTEX_COUNT; vertex++) { + walker.x[vertex] += walker.velocityX[vertex] * deltaSeconds; + walker.y[vertex] += walker.velocityY[vertex] * deltaSeconds; + + if (walker.x[vertex] < PLAY_MARGIN) { + walker.x[vertex] = PLAY_MARGIN; + walker.velocityX[vertex] = std::abs(walker.velocityX[vertex]); + } else if (walker.x[vertex] > 1.0f - PLAY_MARGIN) { + walker.x[vertex] = 1.0f - PLAY_MARGIN; + walker.velocityX[vertex] = -std::abs(walker.velocityX[vertex]); + } + + if (walker.y[vertex] < PLAY_MARGIN) { + walker.y[vertex] = PLAY_MARGIN; + walker.velocityY[vertex] = std::abs(walker.velocityY[vertex]); + } else if (walker.y[vertex] > 1.0f - PLAY_MARGIN) { + walker.y[vertex] = 1.0f - PLAY_MARGIN; + walker.velocityY[vertex] = -std::abs(walker.velocityY[vertex]); + } + } + + for (int age = TRAIL_LENGTH - 1; age > 0; age--) { + std::memcpy(walker.historyX[age], walker.historyX[age - 1], sizeof(walker.x)); + std::memcpy(walker.historyY[age], walker.historyY[age - 1], sizeof(walker.y)); + } + std::memcpy(walker.historyX[0], walker.x, sizeof(walker.x)); + std::memcpy(walker.historyY[0], walker.y, sizeof(walker.y)); +} + +/** The whole surface is the play area: each display fills its own aspect ratio. */ +void drawScene(gfx::Surface& surface, const Walker* walkers, int fps) { + surface.clear(0x0000); + + const float width = static_cast(surface.width()); + const float height = static_cast(surface.height()); + + for (int index = 0; index < WALKER_COUNT; index++) { + const Walker& walker = walkers[index]; + // Draw the oldest echo first so the newest polygon lands on top. + for (int age = TRAIL_LENGTH - 1; age >= 0; age--) { + const int brightness = 40 + (TRAIL_LENGTH - 1 - age) * 215 / (TRAIL_LENGTH - 1); + const uint16_t color = fade565(walker.color, brightness); + + gfx::Point points[VERTEX_COUNT]; + for (int vertex = 0; vertex < VERTEX_COUNT; vertex++) { + points[vertex] = gfx::Point { + static_cast(walker.historyX[age][vertex] * width + 0.5f), + static_cast(walker.historyY[age][vertex] * height + 0.5f), + }; + } + surface.drawPolygon(points, VERTEX_COUNT, color); + } + } + + const uint8_t textScale = static_cast( + std::min(4, std::max(1, std::min(surface.width(), surface.height()) / 150)) + ); + const int margin = 4 * textScale; + + char info[48]; + std::snprintf(info, sizeof(info), "%dx%d %d fps", surface.width(), surface.height(), fps); + surface.drawText(margin, margin, info, gfx::FONT_5X7, 0x9CF3, 0x0000, textScale); + surface.drawText( + margin, + surface.height() - 11 * textScale, + "any input exits", + gfx::FONT_5X7, + 0x632C, + 0x0000, + textScale + ); +} + +bool takeExit(struct AppEventSubscription& subscription, AnyInput& input) { + bool shouldExit = false; + struct AppEvent event {}; + while (app_event_poll(&subscription, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldExit = true; + } + } + return shouldExit || input.activity(); +} + +/** + * A DisplaySink variant that prefers PSRAM for the frame buffer. + * + * `gfx::DisplaySink` requires DMA-capable memory, and ESP-IDF doesn't tag PSRAM as + * `MALLOC_CAP_DMA` on ESP32-P4 even though the SPI DMA engine can read from it (with + * `SPI_TRANS_DMA_USE_PSRAM`, which the ILI9341 driver sets). Requiring internal RAM for a + * 320x240 frame (153.6 KB) would waste most of it, so this sink asks for PSRAM first and lets + * Tactility fall back to any suitable memory when there is none. + */ +class ExternalFirstDisplaySink : public gfx::DisplaySink { +public: + explicit ExternalFirstDisplaySink(struct Device* display) : gfx::DisplaySink(display) {} + + bool frame_buffer_policy(MemoryPolicy& out) const override { + out = MemoryPolicy { 0, MEMORY_CAPABILITY_EXTERNAL, 64 }; + return true; + } +}; + +/** + * The module's API. It is looked up while the app runs instead of linked, and holding the module + * reference keeps the panel module from being stopped (e.g. from Kernel settings) mid-demo. + */ +struct PanelApi { + decltype(&ili9341panel_get_device) getDevice = nullptr; + Module* module = nullptr; +}; + +bool acquirePanelApi(PanelApi& api) { + Module* module = nullptr; + + uintptr_t address = 0; + Module* owner = nullptr; + if (!module_resolve_symbol_global_get("ili9341panel_get_device", &address, &owner)) { + return false; + } + api.getDevice = reinterpret_cast(address); + module = owner; + + api.module = module; + return true; +} + +void releasePanelApi(PanelApi& api) { + if (api.module != nullptr) { + module_put(api.module); + api.module = nullptr; + } +} + +} // namespace + +const char* runApplication(struct Device* display, AnyInput& input) { + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + struct AppEventSubscription subscription {}; + check(app_event_subscribe(&subscription, &event_group) == ERROR_NONE); + + PanelApi api; + if (!acquirePanelApi(api)) { + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&event_group); + return "The ILI9341 panel module is not available. Install or enable the package."; + } + + struct Device* lcd = nullptr; + if (api.getDevice(&lcd) != ERROR_NONE || lcd == nullptr) { + releasePanelApi(api); + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&event_group); + return "The ILI9341 panel is not active. Check the wiring and the panel settings."; + } + + const int lcd_width = display_get_resolution_x(lcd); + const int lcd_height = display_get_resolution_y(lcd); + + ExternalFirstDisplaySink sink(lcd); + gfx::SinkTarget lcd_target(sink, lcd_width, lcd_height, display_get_color_format(lcd)); + if (!lcd_target.valid()) { + device_put(lcd); + releasePanelApi(api); + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&event_group); + return "The Gfx sink could not allocate a frame for the panel."; + } + + // The primary display shows the same scene. LVGL is stopped by the app entry point, so this + // owns the panel until the app exits. + gfx::Target primary(display); + if (primary.valid()) { + const gfx::Info& info = primary.info(); + if (info.width < info.height) { + const gfx::Transform transform { + PRIMARY_MIRROR_WIDTH, + PRIMARY_MIRROR_HEIGHT, + PRIMARY_LANDSCAPE_ROTATION, + }; + if (!primary.set_transform(transform)) { + ESP_LOGW(TAG, "Primary rotation unavailable; keeping native orientation"); + } + } + } else { + ESP_LOGW(TAG, "Primary display can't be used for direct rendering; showing the SPI panel only"); + } + + Walker walkers[WALKER_COUNT]; + resetWalker(walkers[0], 0x07FF); // cyan + resetWalker(walkers[1], 0xF81F); // magenta + + int frames_this_second = 0; + int fps = 0; + int64_t measurement_start_us = esp_timer_get_time(); + + auto render_scene = [&](gfx::Surface& surface) { + drawScene(surface, walkers, fps); + }; + + // Unmistakable write-path check before the animation: red, green, blue, white, black. Both + // screens show the same sequence. + ESP_LOGI(TAG, "Self-test: red, green, blue, white, black"); + constexpr uint16_t SELF_TEST_COLORS[] = { 0xF800, 0x07E0, 0x001F, 0xFFFF, 0x0000 }; + for (const uint16_t color : SELF_TEST_COLORS) { + lcd_target.render([color](gfx::Surface& surface) { + surface.clear(color); + }); + if (primary.valid()) { + primary.render([color](gfx::Surface& surface) { + surface.clear(color); + }); + } + tt::kernel::delayTicks(tt::kernel::millisToTicks(150)); + } + + ESP_LOGI( + TAG, + "Running Mystify at up to %d fps on the panel%s", + TARGET_FPS, + primary.valid() ? " and the primary display" : "" + ); + + int64_t last_frame_us = esp_timer_get_time(); + + while (!takeExit(subscription, input)) { + const int64_t frame_start_us = esp_timer_get_time(); + float deltaSeconds = static_cast(frame_start_us - last_frame_us) / 1'000'000.0f; + last_frame_us = frame_start_us; + deltaSeconds = std::min(deltaSeconds, 0.15f); + + for (auto& walker : walkers) { + moveWalker(walker, deltaSeconds); + } + + lcd_target.render(render_scene); + if (primary.valid()) { + primary.render(render_scene); + } + + frames_this_second++; + const int64_t now_us = esp_timer_get_time(); + if (now_us - measurement_start_us >= 1'000'000) { + fps = frames_this_second; + const gfx::Stats& stats = lcd_target.stats(); + ESP_LOGI( + TAG, + "%d fps, %d dirty px, present %d us, total %d us", + fps, + stats.dirtyPixels, + stats.presentUs, + stats.totalUs + ); + frames_this_second = 0; + measurement_start_us = now_us; + } + + const int64_t remaining_us = FRAME_BUDGET_US - (now_us - frame_start_us); + if (remaining_us > 0) { + tt::kernel::delayTicks(tt::kernel::millisToTicks(static_cast(remaining_us / 1000))); + } + } + + // Leave both panels black: their images otherwise stay on screen after the app exits. + lcd_target.render([](gfx::Surface& surface) { surface.clear(0x0000); }); + if (primary.valid()) { + primary.render([](gfx::Surface& surface) { surface.clear(0x0000); }); + } + + device_put(lcd); + releasePanelApi(api); + + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&event_group); + return nullptr; +} diff --git a/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Main.cpp b/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Main.cpp new file mode 100644 index 0000000..020bacf --- /dev/null +++ b/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Main.cpp @@ -0,0 +1,98 @@ +#include "Application.h" + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +constexpr auto* TAG = "MystifyDemo"; + +// Shows a blocking error dialog and waits for it to close before the caller finishes this app. +static void showErrorAndWait(AppInstanceId appInstanceId, const char* message) { + const char* argv[] = { "Error", message, "OK" }; + uint32_t dialogInstanceId = 0; + app_start_for_result("tactility.alertdialog", 3, argv, appInstanceId, &dialogInstanceId); + if (dialogInstanceId == 0) { + return; + } + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + struct AppEventSubscription sub {}; + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + + while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool done = false; + struct AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + done = true; + break; + } + if (event.type == APP_EVENT_RESULT && event.result.launch_id == dialogInstanceId) { + app_manager_stop(dialogInstanceId); + done = true; + break; + } + } + if (done) break; + } + + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); +} + +extern "C" { + +int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + + AppInstanceId app_instance_id = app_scheduler_current_app_id(); + + struct Device* display_device = nullptr; + if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) { + ESP_LOGE(TAG, "No display device found"); + showErrorAndWait(app_instance_id, "No display device was found."); + return 0; + } + + const char* error = nullptr; + { + AnyInput input; + if (!input.available()) { + error = "No input device was found."; + } else { + // The app draws to both displays directly, so LVGL has to release the primary panel. + module_stop(&lvgl_module); + error = runApplication(display_device, input); + } + } + + device_put(display_device); + if (!module_is_started(&lvgl_module)) { + module_start(&lvgl_module); + } + + if (error != nullptr) { + ESP_LOGE(TAG, "%s", error); + showErrorAndWait(app_instance_id, error); + } + + return 0; +} + +} diff --git a/Apps/MystifySpiDemo/paneldashboard/CMakeLists.txt b/Apps/MystifySpiDemo/paneldashboard/CMakeLists.txt new file mode 100644 index 0000000..f3f1427 --- /dev/null +++ b/Apps/MystifySpiDemo/paneldashboard/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(tactility.mystifyspidemo.paneldashboard) +project(tactility.mystifyspidemo.paneldashboard) +tactility_project_post(tactility.mystifyspidemo.paneldashboard) diff --git a/Apps/MystifySpiDemo/paneldashboard/main/CMakeLists.txt b/Apps/MystifySpiDemo/paneldashboard/main/CMakeLists.txt new file mode 100644 index 0000000..b26d03e --- /dev/null +++ b/Apps/MystifySpiDemo/paneldashboard/main/CMakeLists.txt @@ -0,0 +1,8 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +tactility_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS ../../common +) diff --git a/Apps/MystifySpiDemo/paneldashboard/main/Source/main.cpp b/Apps/MystifySpiDemo/paneldashboard/main/Source/main.cpp new file mode 100644 index 0000000..569ea1d --- /dev/null +++ b/Apps/MystifySpiDemo/paneldashboard/main/Source/main.cpp @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "panel_display.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace { + +constexpr int32_t REFRESH_PERIOD_MS = 1000; + +struct PanelApi { + decltype(&ili9341panel_get_device) getDevice = nullptr; + decltype(&ili9341panel_is_active) isActive = nullptr; + Module* module = nullptr; +}; + +struct Context { + AppInstanceId appInstanceId = 0; + PanelApi api {}; + bool moduleAvailable = false; + bool panelReady = false; + + struct Device* device = nullptr; // held while the app runs + lv_display_t* display = nullptr; + + lv_obj_t* panelRoot = nullptr; + lv_obj_t* panelScreen = nullptr; + lv_obj_t* timeLabel = nullptr; + lv_obj_t* dateLabel = nullptr; + lv_obj_t* uptimeLabel = nullptr; + lv_obj_t* heapLabel = nullptr; + lv_obj_t* psramLabel = nullptr; + lv_timer_t* timer = nullptr; + + lv_obj_t* windowStatusLabel = nullptr; +}; + +bool acquireApi(PanelApi& api) { + Module* module = nullptr; + + auto resolve = [&module](const char* name, auto& function) { + uintptr_t address = 0; + Module* owner = nullptr; + if (!module_resolve_symbol_global_get(name, &address, &owner)) { + return false; + } + function = reinterpret_cast>(address); + if (module == nullptr) { + module = owner; + } else { + module_put(owner); + } + return true; + }; + + const bool found = resolve("ili9341panel_get_device", api.getDevice) && + resolve("ili9341panel_is_active", api.isActive); + if (!found || !module_is_started(module)) { + if (module != nullptr) { + module_put(module); + } + api = PanelApi {}; + return false; + } + + api.module = module; + return true; +} + +void releaseApi(PanelApi& api) { + if (api.module != nullptr) { + module_put(api.module); + } + api = PanelApi {}; +} + +/** Small grey text used for the panel's secondary rows. */ +lv_obj_t* createPanelLabel(lv_obj_t* parent, const char* text, uint32_t color) { + auto* label = lv_label_create(parent); + lv_label_set_text(label, text); + lv_obj_set_style_text_font(label, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT); + lv_obj_set_style_text_color(label, lv_color_hex(color), LV_STATE_DEFAULT); + return label; +} + +/** Full-width label with centred text, so alignment doesn't depend on flex item sizing. */ +void centerPanelLabel(lv_obj_t* label) { + lv_obj_set_width(label, LV_PCT(100)); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT); +} + +void updateStats(Context& ctx) { + if (ctx.timeLabel == nullptr) { + return; + } + + char text[48]; + + const time_t now = time(nullptr); + struct tm* local = localtime(&now); + if (local != nullptr) { + strftime(text, sizeof(text), "%H:%M:%S", local); + lv_label_set_text(ctx.timeLabel, text); + strftime(text, sizeof(text), "%a %d %b", local); + lv_label_set_text(ctx.dateLabel, text); + } else { + lv_label_set_text(ctx.timeLabel, "--:--:--"); + lv_label_set_text(ctx.dateLabel, "no clock"); + } + + const uint64_t totalSeconds = static_cast(esp_timer_get_time()) / 1000000; + const unsigned days = static_cast(totalSeconds / 86400); + const unsigned hours = static_cast((totalSeconds / 3600) % 24); + const unsigned minutes = static_cast((totalSeconds / 60) % 60); + const unsigned seconds = static_cast(totalSeconds % 60); + if (days > 0) { + std::snprintf(text, sizeof(text), "up %ud %02u:%02u", days, hours, minutes); + } else { + std::snprintf(text, sizeof(text), "up %02u:%02u:%02u", hours, minutes, seconds); + } + lv_label_set_text(ctx.uptimeLabel, text); + + const size_t internal_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + std::snprintf(text, sizeof(text), "RAM %uK", static_cast(internal_free / 1024)); + lv_label_set_text(ctx.heapLabel, text); + + const size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + if (psram_free > 0) { + std::snprintf( + text, + sizeof(text), + "PSRAM %u.%uM", + static_cast(psram_free / (1024 * 1024)), + static_cast((psram_free / (1024 * 102)) % 10) + ); + lv_label_set_text(ctx.psramLabel, text); + } else { + lv_label_set_text(ctx.psramLabel, "PSRAM -"); + } +} + +void onTimer(lv_timer_t* timer) { + auto* ctx = static_cast(lv_timer_get_user_data(timer)); + updateStats(*ctx); +} + +/** Builds the dashboard on the panel's own lv_display. Call with the LVGL lock held. */ +void createPanelUi(Context& ctx) { + if (ctx.display == nullptr || ctx.device == nullptr) { + return; + } + + lv_obj_t* screen = lv_display_get_screen_active(ctx.display); + if (screen == nullptr) { + return; + } + + // Paint the screen as well as the container: a display that didn't get the primary theme can + // otherwise show its own (bright) background behind the labels. + lv_obj_set_style_bg_color(screen, lv_color_hex(0x0D1418), LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_STATE_DEFAULT); + ctx.panelScreen = screen; + + auto* root = lv_obj_create(screen); + lv_obj_remove_style_all(root); + lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(root, lv_color_hex(0x0D1418), LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(root, LV_OPA_COVER, LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(root, 14, LV_STATE_DEFAULT); + lv_obj_set_style_pad_gap(root, 4, LV_STATE_DEFAULT); + lv_obj_set_scrollable(root, false); + // A real column layout: header row, a growing centred block, footer. Nothing is absolutely + // positioned, so both 320x240 and 240x320 lay out without overlap. + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + + // Header + auto* headerRow = lv_obj_create(root); + lv_obj_remove_style_all(headerRow); + lv_obj_set_size(headerRow, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(headerRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(headerRow, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_scrollable(headerRow, false); + createPanelLabel(headerRow, "TAB5", 0x5FA8C8); + auto* resolution = createPanelLabel(headerRow, "", 0x5FA8C8); + lv_label_set_text_fmt( + resolution, + "%ux%u", + static_cast(display_get_resolution_x(ctx.device)), + static_cast(display_get_resolution_y(ctx.device)) + ); + + // Clock block, centred in the space between the header and the footer + auto* center = lv_obj_create(root); + lv_obj_remove_style_all(center); + lv_obj_set_width(center, LV_PCT(100)); + lv_obj_set_height(center, LV_SIZE_CONTENT); + lv_obj_set_flex_grow(center, 1); + lv_obj_set_flex_flow(center, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(center, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_gap(center, 2, LV_STATE_DEFAULT); + lv_obj_set_scrollable(center, false); + + ctx.timeLabel = lv_label_create(center); + centerPanelLabel(ctx.timeLabel); + lv_obj_set_style_text_font(ctx.timeLabel, lvgl_get_text_font(FONT_SIZE_LARGE), LV_STATE_DEFAULT); + lv_obj_set_style_text_color(ctx.timeLabel, lv_color_hex(0xF2F7FA), LV_STATE_DEFAULT); + + ctx.dateLabel = createPanelLabel(center, "", 0x8AA4B2); + centerPanelLabel(ctx.dateLabel); + + // Footer: three stacked lines, each short enough for the narrow orientation + auto* footer = lv_obj_create(root); + lv_obj_remove_style_all(footer); + lv_obj_set_size(footer, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(footer, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_gap(footer, 2, LV_STATE_DEFAULT); + lv_obj_set_scrollable(footer, false); + + ctx.uptimeLabel = createPanelLabel(footer, "up", 0x8AA4B2); + ctx.heapLabel = createPanelLabel(footer, "RAM", 0x8AA4B2); + ctx.psramLabel = createPanelLabel(footer, "PSRAM", 0x8AA4B2); + centerPanelLabel(ctx.uptimeLabel); + centerPanelLabel(ctx.heapLabel); + centerPanelLabel(ctx.psramLabel); + + ctx.panelRoot = root; + updateStats(ctx); +} + +void destroyPanelUi(Context& ctx) { + lvgl_lock(); + if (ctx.timer != nullptr) { + lv_timer_delete(ctx.timer); + ctx.timer = nullptr; + } + if (ctx.panelRoot != nullptr) { + lv_obj_delete(ctx.panelRoot); + ctx.panelRoot = nullptr; + ctx.timeLabel = nullptr; + ctx.dateLabel = nullptr; + ctx.uptimeLabel = nullptr; + ctx.heapLabel = nullptr; + ctx.psramLabel = nullptr; + } + if (ctx.panelScreen != nullptr) { + // Leave the panel dark rather than a stale dashboard-coloured screen. + lv_obj_set_style_bg_color(ctx.panelScreen, lv_color_hex(0x000000), LV_STATE_DEFAULT); + ctx.panelScreen = nullptr; + } + lvgl_unlock(); +} + +// region Window + +void onClose(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + app_event_emit_close(ctx->appInstanceId); +} + +void createWidgets(lv_obj_t* root, void* userData) { + auto* ctx = static_cast(userData); + + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_scrollable(root, false); + + auto* toolbar = lvgl_toolbar_create(root, "Panel Dashboard"); + lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClose, ctx); + + auto* content = lv_obj_create(root); + lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(content, LV_PCT(100)); + lv_obj_set_flex_grow(content, 1); + lv_obj_set_style_pad_all(content, 12, LV_STATE_DEFAULT); + lv_obj_set_style_pad_gap(content, 8, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(content, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, LV_STATE_DEFAULT); + + auto* card = lvgl_card_create(content); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_gap(card, 8, LV_STATE_DEFAULT); + + auto* title = lv_label_create(card); + lv_label_set_text(title, "Panel display"); + lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_LARGE), LV_STATE_DEFAULT); + + ctx->windowStatusLabel = lv_label_create(card); + lv_obj_set_width(ctx->windowStatusLabel, LV_PCT(100)); + lv_label_set_long_mode(ctx->windowStatusLabel, LV_LABEL_LONG_WRAP); + + if (ctx->panelReady) { + lv_label_set_text( + ctx->windowStatusLabel, + "The 320x240 panel is showing a live clock, uptime and memory stats. It keeps updating " + "while this app runs and returns to a blank display when you close it." + ); + } else if (ctx->moduleAvailable) { + lv_label_set_text( + ctx->windowStatusLabel, + "The panel module is running, but the panel isn't active. Check its wiring and settings " + "in ILI9341 Panel." + ); + } else { + lv_label_set_text( + ctx->windowStatusLabel, + "The panel module is stopped. Start it in ILI9341 Panel or Kernel settings, then reopen " + "this app." + ); + } +} + +void destroyWidgets(void* userData) { + auto* ctx = static_cast(userData); + ctx->windowStatusLabel = nullptr; +} + +// endregion + +} // namespace + +extern "C" int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + + AppInstanceId app_instance_id = app_scheduler_current_app_id(); + + Context ctx; + ctx.appInstanceId = app_instance_id; + ctx.moduleAvailable = acquireApi(ctx.api); + + if (ctx.moduleAvailable) { + if (ctx.api.getDevice(&ctx.device) == ERROR_NONE && ctx.device != nullptr) { + // A module started at runtime (Kernel settings) has no LVGL display yet; bind it. + if (ili9341panelui::findLvglDisplay(ctx.device) == nullptr) { + ili9341panelui::attachToLvgl(ctx.device); + } + ctx.display = ili9341panelui::findLvglDisplay(ctx.device); + ctx.panelReady = ctx.display != nullptr; + } + } + + if (ctx.panelReady) { + lvgl_lock(); + createPanelUi(ctx); + ctx.timer = lv_timer_create(onTimer, REFRESH_PERIOD_MS, &ctx); + lvgl_unlock(); + } + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + struct AppEventSubscription subscription {}; + check(app_event_subscribe(&subscription, &event_group) == ERROR_NONE); + + WindowId window = window_manager_create_ext(app_instance_id, createWidgets, destroyWidgets, &ctx); + + bool shouldClose = false; + while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + struct AppEvent event; + while (app_event_poll(&subscription, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } + } + } + + window_manager_remove(window); + destroyPanelUi(ctx); + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&event_group); + + if (ctx.device != nullptr) { + device_put(ctx.device); + } + releaseApi(ctx.api); + + return 0; +} diff --git a/Apps/SerialConsole/main/Source/ConnectView.cpp b/Apps/SerialConsole/main/Source/ConnectView.cpp index 994a61e..5417cf5 100644 --- a/Apps/SerialConsole/main/Source/ConnectView.cpp +++ b/Apps/SerialConsole/main/Source/ConnectView.cpp @@ -53,7 +53,7 @@ lv_obj_t* createRowWrapper(lv_obj_t* parent) { void showError(Context* app, const char* message) { const char* argv[] = { "Error", message, "OK" }; uint32_t dialogInstanceId = 0; - app_start_for_result("AlertDialog", 3, argv, app->appInstanceId, &dialogInstanceId); + app_start_for_result("tactility.alertdialog", 3, argv, app->appInstanceId, &dialogInstanceId); } void onConnect(Context* app) { diff --git a/Apps/SerialConsole/main/Source/SerialConsole.cpp b/Apps/SerialConsole/main/Source/SerialConsole.cpp index cc0faae..3461087 100644 --- a/Apps/SerialConsole/main/Source/SerialConsole.cpp +++ b/Apps/SerialConsole/main/Source/SerialConsole.cpp @@ -42,14 +42,14 @@ void showConsoleView(Context* app, Device* dev) { stopActiveView(app); app->activeView = Context::ActiveView::Console; consoleViewCreate(app->wrapperWidget, app, dev); - lv_obj_remove_flag(app->disconnectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(app->disconnectButton, false); } void showConnectView(Context* app) { stopActiveView(app); app->activeView = Context::ActiveView::Connect; connectViewCreate(app->wrapperWidget, app); - lv_obj_add_flag(app->disconnectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(app->disconnectButton, true); } void serialConsoleCreateWidgets(lv_obj_t* parent, void* userData) { @@ -81,11 +81,11 @@ void serialConsoleCreateWidgets(lv_obj_t* parent, void* userData) { // destroys the widget tree, never our background threads or the open UART, so just // rebuild the widgets around the still-running session instead of dropping it (see // ConsoleView.h's comment on consoleViewRebuildWidgets()). - lv_obj_remove_flag(ctx->disconnectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->disconnectButton, false); consoleViewRebuildWidgets(ctx->wrapperWidget, ctx); } else { // First creation, or resurfacing on the (stateless) connect form. - lv_obj_add_flag(ctx->disconnectButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->disconnectButton, true); connectViewCreate(ctx->wrapperWidget, ctx); ctx->activeView = Context::ActiveView::Connect; } diff --git a/Apps/SerialConsole/manifest.properties b/Apps/SerialConsole/manifest.properties index 4b3f149..e542053 100644 --- a/Apps/SerialConsole/manifest.properties +++ b/Apps/SerialConsole/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.serialconsole -version.name=0.20.0 -version.code=20 +version.name=0.21.0 +version.code=21 name=Serial Console app.0.id=tactility.serialconsole app.0.name=Serial Console diff --git a/Apps/Snake/main/Source/Snake.cpp b/Apps/Snake/main/Source/Snake.cpp index 13cf5f9..d000c81 100644 --- a/Apps/Snake/main/Source/Snake.cpp +++ b/Apps/Snake/main/Source/Snake.cpp @@ -31,19 +31,6 @@ constexpr size_t DIFFICULTY_COUNT = 4; // Hell uses same size as Hard but with wall collision enabled const uint16_t difficultySizes[DIFFICULTY_COUNT] = { SNAKE_CELL_LARGE, SNAKE_CELL_MEDIUM, SNAKE_CELL_SMALL, SNAKE_CELL_SMALL }; -uint32_t getToolbarHeight(UiDensity uiDensity) { - if (uiDensity == LVGL_UI_DENSITY_COMPACT) { - return lvgl_get_text_font_height(FONT_SIZE_DEFAULT) * 1.4f; - } else { - return lvgl_get_text_font_height(FONT_SIZE_LARGE) * 2.2f; - } -} - -uint32_t getActionIconPadding(UiDensity uiDensity) { - auto toolbar_height = getToolbarHeight(uiDensity); - return (uiDensity != LVGL_UI_DENSITY_COMPACT) ? (uint32_t)(toolbar_height * 0.2f) : 8; -} - bool getPreferencesPath(std::string& outPath) { char path[128]; if (app_paths_get_user_data_path("tactility.snake", "snake.properties", path, sizeof(path)) != ERROR_NONE) { @@ -128,7 +115,7 @@ void snakeEventCb(lv_event_t* e) { score, length, getHighScore(ctx, ctx->currentDifficulty)); } const char* argv[] = { alertTitle, message, "OK" }; - app_start_for_result("AlertDialog", 3, argv, ctx->appInstanceId, &ctx->gameOverDialogId); + app_start_for_result("tactility.alertdialog", 3, argv, ctx->appInstanceId, &ctx->gameOverDialogId); } else { // Update score display lv_label_set_text_fmt(ctx->scoreLabel, "SCORE: %u", snake_get_score(ctx->gameObject)); @@ -149,7 +136,7 @@ void newGameBtnEvent(lv_event_t* e) { } void createGame(Context* ctx, lv_obj_t* parent, uint16_t cell_size, bool wallCollision, lv_obj_t* tb) { - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); // Create game widget @@ -171,7 +158,7 @@ void createGame(Context* ctx, lv_obj_t* parent, uint16_t cell_size, bool wallCol lv_obj_set_style_pad_column(ctx->scoreWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(ctx->scoreWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ctx->scoreWrapper, 0, LV_STATE_DEFAULT); - lv_obj_remove_flag(ctx->scoreWrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->scoreWrapper, false); // Create score label ctx->scoreLabel = lv_label_create(ctx->scoreWrapper); @@ -183,28 +170,8 @@ void createGame(Context* ctx, lv_obj_t* parent, uint16_t cell_size, bool wallCol lv_obj_set_style_text_color(ctx->scoreLabel, lv_palette_main(LV_PALETTE_GREEN), LV_PART_MAIN); lv_obj_add_event_cb(ctx->gameObject, snakeEventCb, LV_EVENT_VALUE_CHANGED, ctx); - auto ui_density = lvgl_get_ui_density(); - auto toolbar_height = getToolbarHeight(ui_density); - auto icon_padding = getActionIconPadding(ui_density); - - // Create new game button wrapper - ctx->newGameWrapper = lv_obj_create(tb); - lv_obj_set_width(ctx->newGameWrapper, LV_SIZE_CONTENT); - lv_obj_set_flex_flow(ctx->newGameWrapper, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_all(ctx->newGameWrapper, icon_padding / 2, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ctx->newGameWrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ctx->newGameWrapper, 0, LV_STATE_DEFAULT); - // Create new game button - lv_obj_t* newGameBtn = lv_btn_create(ctx->newGameWrapper); - lv_obj_set_size(newGameBtn, toolbar_height - icon_padding, toolbar_height - icon_padding); - lv_obj_set_style_pad_all(newGameBtn, 0, LV_STATE_DEFAULT); - lv_obj_align(newGameBtn, LV_ALIGN_CENTER, 0, 0); - lv_obj_add_event_cb(newGameBtn, newGameBtnEvent, LV_EVENT_CLICKED, ctx); - - lv_obj_t* btnIcon = lv_image_create(newGameBtn); - lv_image_set_src(btnIcon, LV_SYMBOL_REFRESH); - lv_obj_align(btnIcon, LV_ALIGN_CENTER, 0, 0); + lvgl_toolbar_add_text_button_action(tb, LV_SYMBOL_REFRESH, newGameBtnEvent, ctx); } } // namespace @@ -212,7 +179,7 @@ void createGame(Context* ctx, lv_obj_t* parent, uint16_t cell_size, bool wallCol void snakeCreateWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); // Create toolbar @@ -227,7 +194,7 @@ void snakeCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_row(ctx->mainWrapper, 2, LV_PART_MAIN); lv_obj_set_style_pad_column(ctx->mainWrapper, 2, LV_PART_MAIN); lv_obj_set_style_border_width(ctx->mainWrapper, 0, LV_PART_MAIN); - lv_obj_remove_flag(ctx->mainWrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->mainWrapper, false); // Load high scores on first build if (!ctx->highScoresLoaded) { diff --git a/Apps/Snake/main/Source/SnakeUi.c b/Apps/Snake/main/Source/SnakeUi.c index 9f448ab..3a9fde2 100644 --- a/Apps/Snake/main/Source/SnakeUi.c +++ b/Apps/Snake/main/Source/SnakeUi.c @@ -191,7 +191,7 @@ static void snake_timer_cb(lv_timer_t* timer) { if (!snake_spawn_food(game)) { // Hide food since there's no valid position if (game->food) { - lv_obj_add_flag(game->food, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(game->food, true); } // Player won - end the game game->game_over = true; @@ -322,20 +322,20 @@ lv_obj_t* snake_create(lv_obj_t* parent, uint16_t cell_size, bool wall_collision // Set up main object lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100)); - lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(obj, false); lv_obj_set_style_pad_all(obj, 0, LV_PART_MAIN); lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN); lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, LV_PART_MAIN); // Create game container (the actual playing field) game->container = lv_obj_create(obj); - lv_obj_remove_flag(game->container, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(game->container, false); lv_obj_set_style_bg_color(game->container, SNAKE_BG_COLOR, LV_PART_MAIN); lv_obj_set_style_border_width(game->container, 1, LV_PART_MAIN); lv_obj_set_style_border_color(game->container, SNAKE_GRID_COLOR, LV_PART_MAIN); lv_obj_set_style_pad_all(game->container, 0, LV_PART_MAIN); lv_group_remove_obj(game->container); - lv_obj_remove_flag(game->container, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_set_gesture_bubble(game->container, false); // Calculate grid dimensions based on available space (non-square) lv_coord_t available_w = lv_obj_get_content_width(parent) - 6; @@ -459,7 +459,7 @@ void snake_set_new_game(lv_obj_t* obj) { // Spawn new food and unhide it if (snake_spawn_food(game)) { if (game->food) { - lv_obj_remove_flag(game->food, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(game->food, false); } } diff --git a/Apps/Snake/manifest.properties b/Apps/Snake/manifest.properties index 87c947d..f3b1b9f 100644 --- a/Apps/Snake/manifest.properties +++ b/Apps/Snake/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.snake -version.name=0.21.0 -version.code=21 +version.name=0.22.0 +version.code=22 name=Snake description=Classic Snake game app.0.id=tactility.snake diff --git a/Apps/TamaTac/README.md b/Apps/TamaTac/README.md index 19aff48..a3eee93 100644 --- a/Apps/TamaTac/README.md +++ b/Apps/TamaTac/README.md @@ -11,7 +11,7 @@ TamaTac is a digital pet that lives in your ESP32 device. Feed it, play with it, ### Core Pet System - **5 Stats**: Hunger, Happiness, Health, Energy, Cleanliness (0-100%) - **6 Life Stages**: Egg > Baby > Teen > Adult > Elder > Ghost -- **5 Personalities**: Energetic, Lazy, Glutton, Cheerful, Hardy — each affects stat decay rates +- **5 Personalities**: Energetic, Lazy, Glutton, Cheerful, Hardy - each affects stat decay rates - **Automatic Decay**: Stats decay on a 30-second FreeRTOS timer - **Configurable Speed**: Slow (0.5x), Normal (1x), Fast (2x) decay rates - **Sickness**: Health below 20% triggers sick state @@ -41,7 +41,7 @@ Random events occur during timer ticks (~12% chance per tick): - **Had a Nap**: +15 energy - **Sunny Day**: +10 happiness (day only) -Events are contextual — only eligible events trigger based on current stats. +Events are contextual - only eligible events trigger based on current stats. ### Mini-Games @@ -54,11 +54,11 @@ Events are contextual — only eligible events trigger based on current stats. - **Lose immediately**: +5 happiness, -5 energy #### Reaction Time Game -- Large green circle target appears after a random delay (1-3.5 seconds) +- Target circle appears after a random delay (1-3.5 seconds) - Tap as fast as possible when it appears - Tapping too early = penalty round -- 3 rounds, scored by average reaction time -- Performance mapped to happiness reward +- 3 rounds: every in-time tap completes a round; all three fast (≤400ms) earns perfect +- Completion mapped to happiness reward, perfect unlocks Pro Gamer ### Pet Cemetery When your pet dies, it's remembered in the Cemetery: @@ -92,28 +92,39 @@ Uses the shared SfxEngine library: - Sound toggle in Settings ### Responsive Scaling -Adapts to all Tactility screen sizes: -- **Small** (Cardputer 240x135, T-Dongle): Compact layout, 2x sprites -- **Medium** (T-Deck 320x240): Default layout, 3x sprites -- **Large** (480x320 panels): Spacious layout -- **XLarge** (800x480+): Full-size layout +Adapts to all Tactility screen sizes (21 known resolutions, 80x160 to 720x1280): +- **Small** (240x135, 135x240, 170x320-class): Compact layout, 2x sprites +- **Medium** (320x240, 240x320, 320x480, squares): Default layout, 3x sprites +- **Large** (480x480-class): 4x sprites +- **XLarge** (800x480 RGB): 6x sprites, capped 720px centered column +- **XXLarge** (Tab5 720x1280, 1024x600 RGB): 6x sprites, 3x icons, capped 800px column +- Sub-280px tall/wide portrait screens stack pet above stats; all tiers share one + detector (`Layout.h`) instead of per-view pixel checks +- MainView fills its real content area: integer pet scale (2x-8x) refit on layout + and rotation, measured-width button rows (1x4, never-wrapping 2x2), hide-until-settled +- Mini-games size toward the display short side; scrollable list views never clip ### Views - **Main View**: Pet sprite, stat bars with icons, action buttons, poop display, day/night visuals - **Menu View**: Navigation to Stats, Settings, Cemetery, and Achievements - **Stats View**: Detailed stats with exact values, age, life stage, personality -- **Settings View**: Sound toggle, decay speed selector -- **Pattern Game**: Simon Says mini-game -- **Reaction Game**: Reaction time mini-game +- **Settings View**: Sound toggle, decay speed chips (Slow/Normal/Fast) +- **Pattern Game**: Simon Says mini-game (explicit 2x2 rows) +- **Reaction Game**: Reaction time mini-game (completion vs perfect tracked separately) - **Cemetery View**: Hall of fame for past pets - **Achievements View**: Progress tracker for all 12 achievements ### Animated Sprites - 24x24 RGB565 full-color sprites with transparency (magenta color key) - 12 animated pet sprites (2-3 frames each) for all life stages and states -- 9 monochrome 8x8 UI icons for stats and action buttons -- Canvas-rendered with responsive scaling (2x on small screens, 3x on medium+) -- Animation timer at ~5fps for smooth frame cycling +- 9 monochrome 8x8 UI icons for stats and action buttons (3x on XXLarge) +- Canvas-rendered with integer scaling (2x-8x); heap canvas above 72px so small + no-PSRAM devices pay nothing extra +- Pixel-art backdrop: flat day/night colors (band-free on RGB565, fastest load) + + XL/XXL scenery (Flash silhouettes + tileable grass, zero RAM, faded behind + the UI): sun/moon, clouds, ground strip; `gen_bgdecor.py` also renders + composed `decor/scene_day.png` + `scene_night.png` previews +- Animation timer at ~5fps, skipping identical frames to save CPU at large scales #### Sprite Preview @@ -185,7 +196,9 @@ TamaTac/ MainView.h/cpp # Pet display, stat bars, action buttons, day/night MenuView.h/cpp # Navigation menu StatsView.h/cpp # Detailed stats display (incl. personality) - SettingsView.h/cpp # Sound toggle and decay speed settings + SettingsView.h/cpp # Sound toggle and decay speed chips + Layout.h # Shared viewport/size-class detector + content caps + Decor.h # Generated scenery art (see Scripts/sprite_tools/gen_bgdecor.py) PatternGame.h/cpp # Simon Says mini-game ReactionGame.h/cpp # Reaction time mini-game CemeteryView.h/cpp # Pet cemetery / hall of fame @@ -213,12 +226,14 @@ TamaTac uses a multi-view pattern with a shared `wrapperWidget`. Views are swapp ### Save Data -| Namespace | Keys | +One properties file per concern under the app's user-data path: + +| File | Keys | |-----------|------| -| `"TamaTac"` | `hunger`, `happiness`, `health`, `energy`, `cleanliness`, `poopCount`, `ageSeconds`, `ageHours`, `lifespan`, `isSick`, `isAsleep`, `isDead`, `stage`, `currentAnim`, `lastSaveTime`, `personality` | -| `"TamaTacCfg"` | `soundEnabled`, `decaySpeed` | -| `"TamaTacCem"` | `count`, `pers0..4`, `stage0..4`, `age0..4`, `valid0..4` | -| `"TamaTacAch"` | `bits` (uint16 bitfield), `cleanCnt` | +| `pet.properties` | `hunger`, `happiness`, `health`, `energy`, `cleanliness`, `poopCount`, `ageSeconds`, `ageHours`, `lifespan`, `isSick`, `isAsleep`, `isDead`, `stage`, `currentAnim`, `personality`, `lastSaveTime` | +| `tamatac.properties` | `soundOn`, `decaySpd` | +| `cemetery.properties` | `valid0..4`, `pers0..4`, `stage0..4`, `age0..4` | +| `achievements.properties` | `bits` (uint16 bitfield), `cleanCnt` | ## Screenshots diff --git a/Apps/TamaTac/decor/scene_day.png b/Apps/TamaTac/decor/scene_day.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d4787967ff66c1928cfb557a39959068f5c138 GIT binary patch literal 3848 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 literal 0 HcmV?d00001 diff --git a/Apps/TamaTac/decor/scene_night.png b/Apps/TamaTac/decor/scene_night.png new file mode 100644 index 0000000000000000000000000000000000000000..c4487be2acbbac2e2206c572141298a3ea8c733e GIT binary patch literal 1655 zcmeAS@N?(olHy`uVBq!ia0y~yV0-|?d>l+bk$XRa4Hy{M>^xl@Ln`LHy>mWWBwgmf zL(>+c#@G#)nnSq54lcRpS}iz5>5}vB?c3UeyaGQgshIAvq9S0uZO#G9F9A7Syk@cah>xgZ>nX@y-uJ&6^@#~BE{qtqI7jl~zIA$L%c>HVY z>TtdITM_kdm$uK2{{GH>NA2@>-Gy_ua+(;JW*`3WxBULST|dtz+5P{%db-)Q{T9_} z8)y9g@Yw&O+irGVpR>KMum4+p&RXpA&$YAf-`P=@drj@afh92(_3vIyJ^lMig%P=1~R^W}|ZJl6MD zST8u56!@=hivzO@&>6D&#|n4G$o={8v?%i5vbhe-lRtgA`T2Xvhjw+KRXKUKRXcW; zPEeN5%gfEPoz)7|p}F6-uIT;0=BMAj{cG3VzWx2(dV&9ee2r|&o_(9U+y4Kj*LJ;e zdHMhUoL;~E{hhg6=gydKb-{cWW8cKBo_``H0c|LKIOG4F6+j1V|91Vq+spi!@sFRo zHNLDaymM*KVS6Dapt~HVfSor=jfTXqObl0Ey;5I(%>Gzn>4yv^wFD+LgK1Seik`eb zu$Xg31Luqb8^Za+&m3j(Il$tR;PFn}e~!)Hud9E{?vB4*D}Utkvj;T=u`dp^&p+IG zd48>F-+unX%OAfwd3XKXIlb)_|DJyOtbe~w_Vw)k&&R)-)_kdr__@2yI(P2bvZ#Nb zZdqLUyxxxGUJ*>b?n~{Bt<807dv?$JxxfGS`h?ogh5vT!`;utkp!ol$iH;{!=Ko1s`7n7^_vB6i~rgxl=em4uW9T5`sZdf z`(M8QJP;== #include @@ -70,6 +71,12 @@ bool achievementsHas(uint16_t bits, AchievementId id) { return (bits >> static_cast(id)) & 1; } +// NOTE: achievement STORAGE (unlock/increment/clean-count) is serialized by +// Context.timerMutex - every caller below runs with it held (action handlers +// take it, the 30s timer callback holds it for its whole duration). Do not +// call these from a thread that doesn't hold it: concurrent read-modify-write +// of the bitfield/counter can lose an unlock. (The Achievements *view* reads +// without the lock; worst case it shows bits one tick stale.) void achievementsUnlock(AchievementId id) { if (static_cast(id) >= static_cast(AchievementId::COUNT)) { return; @@ -120,10 +127,9 @@ void achievementsIncrementCleanCount() { void achievementsViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { AchievementsViewState* state = &ctx->achievementsView; - lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr); - lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr); - bool isSmall = (screenWidth < 280 || screenHeight < 180); - bool isXLarge = (screenWidth >= 600); + TamaViewport vp = tamaGetViewport(); + bool isSmall = tamaIsSmall(vp); + bool isXLarge = tamaIsXLarge(vp); int padAll = isSmall ? 4 : (isXLarge ? 16 : 8); int padRow = isSmall ? 2 : (isXLarge ? 8 : 4); @@ -136,6 +142,8 @@ void achievementsViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_style_border_width(state->mainWrapper, 0, 0); lv_obj_set_flex_flow(state->mainWrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(state->mainWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + // 12 rows always overflow short screens - vertical scroll only + lv_obj_set_scroll_dir(state->mainWrapper, LV_DIR_VER); uint16_t bits = achievementsLoad(); int unlocked = achievementsCountUnlocked(bits); diff --git a/Apps/TamaTac/main/Source/CemeteryView.cpp b/Apps/TamaTac/main/Source/CemeteryView.cpp index 4c0cba1..b32d8d5 100644 --- a/Apps/TamaTac/main/Source/CemeteryView.cpp +++ b/Apps/TamaTac/main/Source/CemeteryView.cpp @@ -4,6 +4,7 @@ */ #include "CemeteryView.h" +#include "Layout.h" #include "TamaTac.h" #include #include @@ -42,16 +43,25 @@ void cemeteryViewLoadRecords(PetRecord records[CEMETERY_MAX_RECORDS]) { snprintf(key, sizeof(key), "pers%d", i); value = 0; preferences_opt_int32(prefs, key, &value); + if (value < 0 || value > static_cast(Personality::Hardy)) { + value = 0; + } records[i].personality = static_cast(value); snprintf(key, sizeof(key), "stage%d", i); value = 0; preferences_opt_int32(prefs, key, &value); + if (value < 0 || value > static_cast(LifeStage::Ghost)) { + value = static_cast(LifeStage::Egg); + } records[i].stageReached = static_cast(value); snprintf(key, sizeof(key), "age%d", i); value = 0; preferences_opt_int32(prefs, key, &value); + if (value < 0) { + value = 0; + } records[i].ageHours = static_cast(value); } } @@ -112,10 +122,9 @@ void cemeteryViewRecordDeath(Personality personality, LifeStage stage, uint16_t void cemeteryViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { CemeteryViewState* state = &ctx->cemeteryView; - lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr); - lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr); - bool isSmall = (screenWidth < 280 || screenHeight < 180); - bool isXLarge = (screenWidth >= 600); + TamaViewport vp = tamaGetViewport(); + bool isSmall = tamaIsSmall(vp); + bool isXLarge = tamaIsXLarge(vp); int padAll = isSmall ? 4 : (isXLarge ? 16 : 8); int padRow = isSmall ? 4 : (isXLarge ? 12 : 6); @@ -128,6 +137,8 @@ void cemeteryViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_style_border_width(state->mainWrapper, 0, 0); lv_obj_set_flex_flow(state->mainWrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(state->mainWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + // Rows can exceed short screens - vertical scroll only + lv_obj_set_scroll_dir(state->mainWrapper, LV_DIR_VER); // Title lv_obj_t* title = lv_label_create(state->mainWrapper); diff --git a/Apps/TamaTac/main/Source/Decor.h b/Apps/TamaTac/main/Source/Decor.h new file mode 100644 index 0000000..a21f50e --- /dev/null +++ b/Apps/TamaTac/main/Source/Decor.h @@ -0,0 +1,196 @@ +/** + * @file Decor.h + * @brief Background scenery for TamaTac (AUTO-GENERATED by gen_bgdecor.py). + * + * A8 alpha silhouettes (sun/moon/clouds, recolored+faded at runtime) and + * tileable RGB565 grass tiles. Do not hand-edit: + * tweak Scripts/sprite_tools/gen_bgdecor.py and re-run. + */ +#pragma once + +#include +#include + +// A8 alpha silhouette (24x24) +constexpr uint8_t decorSun[576] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x15, 0x44, 0x5C, 0x5C, 0x44, 0x15, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x5C, 0xA9, 0xDF, 0xFC, 0xFC, 0xDF, 0xA9, 0x5C, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x75, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0x75, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5C, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0xA9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA9, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x5C, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xDC, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x5C, 0x00, 0xDC, 0xDC, 0xDC, 0xDC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0xA9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA9, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5C, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x5C, 0xA9, 0xDF, 0xFC, 0xFC, 0xDF, 0xA9, 0x5C, 0x00, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x15, 0x44, 0x5C, 0x5C, 0x44, 0x15, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// A8 alpha silhouette (20x20) +constexpr uint8_t decorMoon[400] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x6F, 0x98, 0xAD, 0xAD, 0x98, 0x6F, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0D, 0x6F, 0xC2, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC2, 0x6F, 0x0D, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0D, 0x83, 0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 0x83, 0x0D, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x6F, 0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 0x6F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x33, 0xC2, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC2, 0x33, 0x00, 0x00, + 0x00, 0x00, 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6F, 0x00, 0x00, + 0x00, 0x00, 0x98, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x00, 0x00, + 0x00, 0x0D, 0xAD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAD, 0x0D, 0x00, + 0x00, 0x0D, 0xAD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAD, 0x0D, 0x00, + 0x00, 0x00, 0x98, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x00, 0x00, + 0x00, 0x00, 0x6F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6F, 0x00, 0x00, + 0x00, 0x00, 0x33, 0xC2, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC2, 0x33, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x6F, 0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 0x6F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0D, 0x83, 0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 0x83, 0x0D, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0D, 0x6F, 0xC2, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC2, 0x6F, 0x0D, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x6F, 0x98, 0xAD, 0xAD, 0x98, 0x6F, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// A8 alpha silhouette (40x20) +constexpr uint8_t cloudBig[800] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x0D, 0x13, 0x17, 0x1B, 0x1D, 0x1D, 0x1D, 0x1B, 0x17, 0x13, 0x0D, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x13, 0x1C, 0x24, 0x2A, 0x2F, 0x33, 0x36, 0x36, 0x36, 0x33, 0x2F, 0x2A, 0x24, 0x1C, 0x13, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x11, 0x1D, 0x27, 0x31, 0x3A, 0x41, 0x47, 0x4C, 0x4F, 0x4F, 0x4F, 0x4C, 0x47, 0x41, 0x3A, 0x31, 0x27, 0x1D, 0x11, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x15, 0x22, 0x2F, 0x3B, 0x45, 0x4F, 0x58, 0x5F, 0x64, 0x67, 0x68, 0x67, 0x64, 0x5F, 0x58, 0x4F, 0x45, 0x3B, 0x2F, 0x22, 0x15, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x07, 0x15, 0x23, 0x31, 0x3F, 0x4C, 0x58, 0x63, 0x6D, 0x76, 0x7C, 0x80, 0x81, 0x80, 0x7C, 0x76, 0x6D, 0x63, 0x58, 0x4C, 0x3F, 0x31, 0x23, 0x15, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x0F, 0x18, 0x1F, 0x25, 0x29, 0x2F, 0x3E, 0x4D, 0x5B, 0x68, 0x75, 0x81, 0x8B, 0x93, 0x99, 0x9A, 0x99, 0x93, 0x8B, 0x81, 0x75, 0x68, 0x5B, 0x4D, 0x3E, 0x2F, 0x1F, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x05, 0x12, 0x1E, 0x29, 0x33, 0x3B, 0x42, 0x47, 0x4B, 0x4C, 0x57, 0x67, 0x76, 0x84, 0x92, 0x9F, 0xA9, 0xB1, 0xB3, 0xB1, 0xA9, 0x9F, 0x92, 0x84, 0x76, 0x67, 0x57, 0x47, 0x37, 0x2A, 0x24, 0x1B, 0x10, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0A, 0x19, 0x27, 0x34, 0x41, 0x4C, 0x57, 0x5F, 0x65, 0x69, 0x6A, 0x69, 0x6E, 0x7E, 0x8E, 0x9E, 0xAE, 0xBC, 0xC7, 0xCC, 0xC7, 0xBC, 0xAE, 0x9E, 0x8E, 0x7E, 0x6E, 0x5E, 0x54, 0x52, 0x4D, 0x45, 0x3B, 0x2E, 0x20, 0x11, 0x00, 0x00, + 0x00, 0x09, 0x1A, 0x2A, 0x39, 0x48, 0x57, 0x64, 0x70, 0x7A, 0x82, 0x87, 0x89, 0x87, 0x82, 0x81, 0x92, 0xA3, 0xB3, 0xC4, 0xD5, 0xE5, 0xD5, 0xC4, 0xB3, 0xA3, 0x92, 0x81, 0x71, 0x76, 0x78, 0x76, 0x70, 0x66, 0x59, 0x4A, 0x39, 0x28, 0x15, 0x02, + 0x03, 0x14, 0x26, 0x37, 0x48, 0x58, 0x68, 0x78, 0x86, 0x93, 0x9E, 0xA5, 0xA8, 0xA5, 0x9E, 0x93, 0x8E, 0x9E, 0xAE, 0xBC, 0xC7, 0xCC, 0xC7, 0xBC, 0xAE, 0x9E, 0x8E, 0x83, 0x90, 0x99, 0x9D, 0x99, 0x90, 0x83, 0x73, 0x61, 0x4E, 0x3A, 0x25, 0x11, + 0x09, 0x1B, 0x2D, 0x3F, 0x51, 0x63, 0x74, 0x86, 0x97, 0xA7, 0xB6, 0xC2, 0xC7, 0xC2, 0xB6, 0xA7, 0x97, 0x92, 0x9F, 0xA9, 0xB1, 0xB3, 0xB1, 0xA9, 0x9F, 0x92, 0x85, 0x9A, 0xAC, 0xBB, 0xC1, 0xBB, 0xAC, 0x9A, 0x85, 0x71, 0x5B, 0x46, 0x30, 0x1A, + 0x0B, 0x1D, 0x30, 0x42, 0x54, 0x66, 0x78, 0x8B, 0x9D, 0xAF, 0xC1, 0xD3, 0xE5, 0xD3, 0xC1, 0xAF, 0x9D, 0x8B, 0x8B, 0x93, 0x99, 0x9A, 0x99, 0x93, 0x8B, 0x81, 0x8D, 0xA3, 0xB9, 0xCF, 0xE5, 0xCF, 0xB9, 0xA3, 0x8D, 0x76, 0x60, 0x4A, 0x34, 0x1D, + 0x09, 0x1B, 0x2D, 0x3F, 0x51, 0x63, 0x74, 0x86, 0x97, 0xA7, 0xB6, 0xC2, 0xC7, 0xC2, 0xB6, 0xA7, 0x97, 0x86, 0x76, 0x7C, 0x80, 0x81, 0x80, 0x7C, 0x76, 0x71, 0x85, 0x9A, 0xAC, 0xBB, 0xC1, 0xBB, 0xAC, 0x9A, 0x85, 0x71, 0x5B, 0x46, 0x30, 0x1A, + 0x03, 0x14, 0x26, 0x37, 0x48, 0x58, 0x68, 0x78, 0x86, 0x93, 0x9E, 0xA5, 0xA8, 0xA5, 0x9E, 0x93, 0x86, 0x78, 0x68, 0x64, 0x67, 0x68, 0x67, 0x64, 0x5F, 0x61, 0x73, 0x83, 0x90, 0x99, 0x9D, 0x99, 0x90, 0x83, 0x73, 0x61, 0x4E, 0x3A, 0x25, 0x11, + 0x00, 0x09, 0x1A, 0x2A, 0x39, 0x48, 0x57, 0x64, 0x70, 0x7A, 0x82, 0x87, 0x89, 0x87, 0x82, 0x7A, 0x70, 0x64, 0x57, 0x4C, 0x4F, 0x4F, 0x4F, 0x4C, 0x47, 0x4A, 0x59, 0x66, 0x70, 0x76, 0x78, 0x76, 0x70, 0x66, 0x59, 0x4A, 0x39, 0x28, 0x15, 0x02, + 0x00, 0x00, 0x0A, 0x19, 0x27, 0x34, 0x41, 0x4C, 0x57, 0x5F, 0x65, 0x69, 0x6A, 0x69, 0x65, 0x5F, 0x57, 0x4C, 0x41, 0x34, 0x36, 0x36, 0x36, 0x33, 0x2F, 0x2E, 0x3B, 0x45, 0x4D, 0x52, 0x54, 0x52, 0x4D, 0x45, 0x3B, 0x2E, 0x20, 0x11, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x05, 0x12, 0x1E, 0x29, 0x33, 0x3B, 0x42, 0x47, 0x4B, 0x4C, 0x4B, 0x47, 0x42, 0x3B, 0x33, 0x29, 0x1E, 0x1D, 0x1D, 0x1D, 0x1B, 0x17, 0x13, 0x1B, 0x24, 0x2A, 0x2E, 0x30, 0x2E, 0x2A, 0x24, 0x1B, 0x10, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x0F, 0x18, 0x1F, 0x25, 0x29, 0x2C, 0x2D, 0x2C, 0x29, 0x25, 0x1F, 0x18, 0x0F, 0x05, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00, 0x00, 0x01, 0x07, 0x0A, 0x0B, 0x0A, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// A8 alpha silhouette (28x14) +constexpr uint8_t cloudSmall[392] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x0A, 0x0B, 0x0A, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x19, 0x22, 0x2A, 0x2E, 0x30, 0x2E, 0x2A, 0x22, 0x19, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x1B, 0x2A, 0x38, 0x44, 0x4D, 0x52, 0x54, 0x52, 0x4D, 0x44, 0x38, 0x2A, 0x1B, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x20, 0x33, 0x45, 0x55, 0x64, 0x6F, 0x76, 0x78, 0x76, 0x6F, 0x64, 0x55, 0x45, 0x33, 0x20, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0x1A, 0x25, 0x2D, 0x32, 0x47, 0x5B, 0x6F, 0x80, 0x8F, 0x99, 0x9D, 0x99, 0x8F, 0x80, 0x6F, 0x5B, 0x47, 0x31, 0x1C, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x09, 0x1D, 0x2F, 0x3F, 0x4D, 0x57, 0x5E, 0x60, 0x6A, 0x81, 0x96, 0xAA, 0xBA, 0xC1, 0xBA, 0xAA, 0x96, 0x81, 0x6A, 0x54, 0x4C, 0x43, 0x35, 0x23, 0x0F, 0x00, + 0x00, 0x0B, 0x21, 0x37, 0x4C, 0x60, 0x71, 0x80, 0x89, 0x8D, 0x89, 0x87, 0x9F, 0xB6, 0xCE, 0xE5, 0xCE, 0xB6, 0x9F, 0x87, 0x7D, 0x81, 0x7D, 0x70, 0x5D, 0x47, 0x2E, 0x14, + 0x00, 0x19, 0x31, 0x49, 0x61, 0x78, 0x8E, 0xA3, 0xB3, 0xB9, 0xB3, 0xA3, 0x96, 0xAA, 0xBA, 0xC1, 0xBA, 0xAA, 0x96, 0x96, 0xAB, 0xB3, 0xAB, 0x96, 0x7D, 0x61, 0x44, 0x26, + 0x04, 0x1D, 0x36, 0x4F, 0x68, 0x81, 0x9A, 0xB3, 0xCC, 0xE5, 0xCC, 0xB3, 0x9A, 0x8F, 0x99, 0x9D, 0x99, 0x8F, 0x89, 0xA8, 0xC7, 0xE5, 0xC7, 0xA8, 0x89, 0x6A, 0x4C, 0x2D, + 0x00, 0x19, 0x31, 0x49, 0x61, 0x78, 0x8E, 0xA3, 0xB3, 0xB9, 0xB3, 0xA3, 0x8E, 0x78, 0x76, 0x78, 0x76, 0x6F, 0x7D, 0x96, 0xAB, 0xB3, 0xAB, 0x96, 0x7D, 0x61, 0x44, 0x26, + 0x00, 0x0B, 0x21, 0x37, 0x4C, 0x60, 0x71, 0x80, 0x89, 0x8D, 0x89, 0x80, 0x71, 0x60, 0x52, 0x54, 0x52, 0x4D, 0x5D, 0x70, 0x7D, 0x81, 0x7D, 0x70, 0x5D, 0x47, 0x2E, 0x14, + 0x00, 0x00, 0x09, 0x1D, 0x2F, 0x3F, 0x4D, 0x57, 0x5E, 0x60, 0x5E, 0x57, 0x4D, 0x3F, 0x2F, 0x30, 0x2E, 0x2A, 0x35, 0x43, 0x4C, 0x4F, 0x4C, 0x43, 0x35, 0x23, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0x1A, 0x25, 0x2D, 0x32, 0x34, 0x32, 0x2D, 0x25, 0x1A, 0x0C, 0x0B, 0x0A, 0x06, 0x09, 0x14, 0x1B, 0x1D, 0x1B, 0x14, 0x09, 0x00, 0x00, 0x00 +}; +// 32x16 RGB565 tileable ground tile +constexpr uint16_t grassDay[512] = { + 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, + 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, + 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2387, + 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x2C68, + 0x7AC6, 0x6225, 0x7AC6, 0x2C68, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x7AC6, 0x6225, 0x7AC6, 0x2C68, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x7AC6, 0x6225, 0x7AC6, 0x2C68, 0x2C68, 0x2387, 0x2C68, 0x2387, 0x7AC6, 0x6225, 0x6225, 0x2C68, 0x2C68, 0x2387, 0x2C68, 0x2387, + 0x6225, 0x7AC6, 0x6225, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x6225, 0x7AC6, 0x6225, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x6225, 0x7AC6, 0x6225, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x6225, 0x7AC6, 0x6225, 0x2C68, 0x2C68, 0x2C68, 0x2C68, 0x2C68, + 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, + 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, + 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, + 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, + 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, + 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, + 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, + 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, + 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, + 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x6225, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6, 0x6225, 0x7AC6 +}; +// 32x16 RGB565 tileable ground tile +constexpr uint16_t grassNight[512] = { + 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, + 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, + 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x08A6, + 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x10C7, + 0x18A5, 0x1063, 0x18A5, 0x10C7, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x18A5, 0x1063, 0x18A5, 0x10C7, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x18A5, 0x1063, 0x18A5, 0x10C7, 0x10C7, 0x08A6, 0x10C7, 0x08A6, 0x18A5, 0x1063, 0x1063, 0x10C7, 0x10C7, 0x08A6, 0x10C7, 0x08A6, + 0x1063, 0x18A5, 0x1063, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x1063, 0x18A5, 0x1063, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x1063, 0x18A5, 0x1063, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x1063, 0x18A5, 0x1063, 0x10C7, 0x10C7, 0x10C7, 0x10C7, 0x10C7, + 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, + 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, + 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, + 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, + 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, + 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, + 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, + 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, + 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, + 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x1063, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5, 0x1063, 0x18A5 +}; + +inline const lv_image_dsc_t decorSunDsc = { + .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_A8, + .flags = 0, .w = 24, .h = 24, .stride = 24, + .reserved_2 = 0 }, + .data_size = 576, + .data = decorSun, + .reserved = nullptr, + .reserved_2 = nullptr, +}; +inline const lv_image_dsc_t decorMoonDsc = { + .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_A8, + .flags = 0, .w = 20, .h = 20, .stride = 20, + .reserved_2 = 0 }, + .data_size = 400, + .data = decorMoon, + .reserved = nullptr, + .reserved_2 = nullptr, +}; +inline const lv_image_dsc_t cloudBigDsc = { + .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_A8, + .flags = 0, .w = 40, .h = 20, .stride = 40, + .reserved_2 = 0 }, + .data_size = 800, + .data = cloudBig, + .reserved = nullptr, + .reserved_2 = nullptr, +}; +inline const lv_image_dsc_t cloudSmallDsc = { + .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_A8, + .flags = 0, .w = 28, .h = 14, .stride = 28, + .reserved_2 = 0 }, + .data_size = 392, + .data = cloudSmall, + .reserved = nullptr, + .reserved_2 = nullptr, +}; +inline const lv_image_dsc_t grassDayDsc = { + .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_RGB565, + .flags = 0, .w = 32, .h = 16, .stride = 64, + .reserved_2 = 0 }, + .data_size = 1024, + .data = reinterpret_cast(grassDay), + .reserved = nullptr, + .reserved_2 = nullptr, +}; +inline const lv_image_dsc_t grassNightDsc = { + .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_RGB565, + .flags = 0, .w = 32, .h = 16, .stride = 64, + .reserved_2 = 0 }, + .data_size = 1024, + .data = reinterpret_cast(grassNight), + .reserved = nullptr, + .reserved_2 = nullptr, +}; diff --git a/Apps/TamaTac/main/Source/Layout.h b/Apps/TamaTac/main/Source/Layout.h new file mode 100644 index 0000000..a22cd0c --- /dev/null +++ b/Apps/TamaTac/main/Source/Layout.h @@ -0,0 +1,124 @@ +/** + * @file Layout.h + * @brief Shared responsive-layout helper for TamaTac views. + * + * Single source of truth for size detection. Uses display resolution + * (parent may not be laid out yet on first load) plus orientation flags + * so portrait-narrow screens (e.g. 135x240) can stack vertically while + * landscape-short screens (e.g. 240x135) stay compact. + * + * Size classes (kept compatible with the legacy tiers): + * Small - (W < 280 || H < 180): Cardputer 240x135, Sticks3 135x240, 170x320, 172x320 + * Medium - default: 320x240, 240x320, 320x480, 480x222, 480x480, square 240x240 + * Large - (W >= 400 && H >= 300, not XLarge): 480x480, split views + * XLarge - (W >= 600): 800x480 RGB panels and similar + * XXLarge - (max(W,H) >= 1000): Tab5 720x1280, 1024x600 RGB, large EPD + * + * Ultra-small (80x160 tdongle, 128x64 OLED) is explicitly unsupported. + */ + +#pragma once + +#include + +enum class TamaSizeClass { + Small, + Medium, + Large, + XLarge, + XXLarge +}; + +struct TamaViewport { + lv_coord_t width = 0; + lv_coord_t height = 0; + TamaSizeClass sizeClass = TamaSizeClass::Medium; + bool isPortrait = false; // height > width + bool isNarrow = false; // width < 300 (e.g. 135-wide portrait) + bool isShort = false; // height < 250 (e.g. 135-high landscape) +}; + +inline TamaViewport tamaGetViewport() { + TamaViewport vp; + vp.width = lv_display_get_horizontal_resolution(nullptr); + vp.height = lv_display_get_vertical_resolution(nullptr); + + if (vp.width < 280 || vp.height < 180) { + vp.sizeClass = TamaSizeClass::Small; + } else if (vp.width >= 1000 || vp.height >= 1000) { + vp.sizeClass = TamaSizeClass::XXLarge; + } else if (vp.width >= 600) { + vp.sizeClass = TamaSizeClass::XLarge; + } else if (vp.width >= 400 && vp.height >= 300) { + vp.sizeClass = TamaSizeClass::Large; + } else { + vp.sizeClass = TamaSizeClass::Medium; + } + + vp.isPortrait = vp.height > vp.width; + vp.isNarrow = vp.width < 300; + vp.isShort = vp.height < 250; + return vp; +} + +inline bool tamaIsSmall(const TamaViewport& vp) { + return vp.sizeClass == TamaSizeClass::Small; +} + +// "At least XLarge" - keeps pre-XXL call sites behaving on big screens. +inline bool tamaIsXLarge(const TamaViewport& vp) { + return vp.sizeClass == TamaSizeClass::XLarge || + vp.sizeClass == TamaSizeClass::XXLarge; +} + +inline bool tamaIsXXLarge(const TamaViewport& vp) { + return vp.sizeClass == TamaSizeClass::XXLarge; +} + +// Standard paddings shared by views (matches legacy values) +inline int tamaPadAll(const TamaViewport& vp) { + switch (vp.sizeClass) { + case TamaSizeClass::Small: return 4; + case TamaSizeClass::XLarge: + case TamaSizeClass::XXLarge: return 16; + default: return 8; + } +} + +inline int tamaPadRow(const TamaViewport& vp) { + switch (vp.sizeClass) { + case TamaSizeClass::Small: return 4; + case TamaSizeClass::XLarge: + case TamaSizeClass::XXLarge: return 16; + default: return 8; + } +} + +inline int tamaGap(const TamaViewport& vp) { + switch (vp.sizeClass) { + case TamaSizeClass::Small: return 4; + case TamaSizeClass::XLarge: + case TamaSizeClass::XXLarge: return 12; + default: return 6; + } +} + +// Max content width for the capped tiers (XLarge/XXLarge) so bars/buttons +// don't stretch absurdly on wide screens. 0 = percent mode (tiers below XL +// follow the parent). Recompute callers need this LIVE (rotation changes +// the display resolution) - never cache it across orientation changes. +inline int tamaContentCap(const TamaViewport& vp) { + if (vp.sizeClass == TamaSizeClass::XXLarge) { + int capped = vp.width - 32; + if (capped > 800) capped = 800; + if (capped < 200) capped = 200; + return capped; + } + if (vp.sizeClass == TamaSizeClass::XLarge) { + int capped = vp.width - 32; + if (capped > 720) capped = 720; + if (capped < 200) capped = 200; + return capped; + } + return 0; +} diff --git a/Apps/TamaTac/main/Source/MainView.cpp b/Apps/TamaTac/main/Source/MainView.cpp index 2080728..32f9a4e 100644 --- a/Apps/TamaTac/main/Source/MainView.cpp +++ b/Apps/TamaTac/main/Source/MainView.cpp @@ -4,10 +4,13 @@ */ #include "MainView.h" +#include "Layout.h" +#include "Decor.h" #include "TamaTac.h" #include #include #include +#include extern lv_color_t TamaTac_canvasBuffer[72 * 72]; extern lv_color_t TamaTac_iconBuffers[12][16 * 16]; @@ -19,7 +22,7 @@ SpriteId getSpriteForCurrentState(const PetStats& stats) { return SPRITE_GHOST; } - // Egg always shows egg — no action/mood sprite overrides + // Egg always shows egg - no action/mood sprite overrides if (stats.stage == LifeStage::Egg) { return SPRITE_EGG_IDLE; } @@ -111,7 +114,7 @@ void drawOverlays(MainViewState* state, SpriteId spriteId, const PetStats* stats } else if (stats->hunger >= 70 && stats->happiness >= 70 && stats->health >= 70 && stats->energy >= 70) { moodColor = lv_color_hex(0x00FF00); // Green for happy } else { - showMood = false; // Neutral — no indicator + showMood = false; // Neutral - no indicator } if (showMood) { @@ -127,6 +130,35 @@ void drawOverlays(MainViewState* state, SpriteId spriteId, const PetStats* stats } } +// Animation frame index for a sprite at a given moment (shared by the draw +// path and the anim-timer skip check so they can never disagree) +int currentFrameIdx(SpriteId spriteId, uint32_t now, uint32_t animStartTime) { + const AnimatedSprite& anim = getAnimSprite(spriteId); + int frameIdx = 0; + if (anim.frameCount > 1 && anim.frameDelayMs > 0) { + uint32_t elapsed = now - animStartTime; + if (anim.loop) { + frameIdx = (elapsed / anim.frameDelayMs) % anim.frameCount; + } else { + frameIdx = elapsed / anim.frameDelayMs; + if (frameIdx >= anim.frameCount) frameIdx = anim.frameCount - 1; + } + } + return frameIdx; +} + +// Overlay animation phase: sleep Z's bob stepwise, night stars twinkle on a +// slower cycle, everything else is static. Distinct ranges per source. +int overlayPhase(SpriteId spriteId, DayPhase dayPhase, uint32_t now) { + if (spriteId == SPRITE_SLEEPING) { + return static_cast((now / 500) % 4); + } + if (dayPhase == DayPhase::Night) { + return 100 + static_cast((now / 2000) % 8); + } + return 0; +} + void drawSprite(MainViewState* state, SpriteId spriteId, const PetStats* stats) { if (state->petCanvas == nullptr) return; @@ -134,9 +166,15 @@ void drawSprite(MainViewState* state, SpriteId spriteId, const PetStats* stats) int canvasSize = SPRITE_WIDTH * state->spriteScale; if (canvasSize > state->petCanvasSize) return; + bool flashing = (state->evolutionFlashUntil > 0 && now < state->evolutionFlashUntil); + // Evolution flash: fill entire canvas with white - if (state->evolutionFlashUntil > 0 && now < state->evolutionFlashUntil) { + if (flashing) { lv_canvas_fill_bg(state->petCanvas, lv_color_hex(0xFFFFFF), LV_OPA_COVER); + state->lastFrameIdx = currentFrameIdx(spriteId, now, state->animStartTime); + state->lastOverlayPhase = overlayPhase(spriteId, state->currentDayPhase, now); + state->lastFlash = true; + state->lastSpriteId = spriteId; return; } else if (state->evolutionFlashUntil > 0) { state->evolutionFlashUntil = 0; @@ -164,16 +202,7 @@ void drawSprite(MainViewState* state, SpriteId spriteId, const PetStats* stats) // Determine current animation frame const AnimatedSprite& anim = getAnimSprite(spriteId); - int frameIdx = 0; - if (anim.frameCount > 1 && anim.frameDelayMs > 0) { - uint32_t elapsed = now - state->animStartTime; - if (anim.loop) { - frameIdx = (elapsed / anim.frameDelayMs) % anim.frameCount; - } else { - frameIdx = elapsed / anim.frameDelayMs; - if (frameIdx >= anim.frameCount) frameIdx = anim.frameCount - 1; - } - } + int frameIdx = currentFrameIdx(spriteId, now, state->animStartTime); // Render RGB565 sprite with transparency and scaling const uint16_t* pixelData = anim.frames[frameIdx].data; @@ -200,9 +229,15 @@ void drawSprite(MainViewState* state, SpriteId spriteId, const PetStats* stats) // Draw overlays (Z's, mood indicator) drawOverlays(state, spriteId, stats); + + // Remember what was drawn so the anim timer can skip identical frames + state->lastFrameIdx = frameIdx; + state->lastOverlayPhase = overlayPhase(spriteId, state->currentDayPhase, now); + state->lastFlash = false; + state->lastSpriteId = spriteId; } -void drawIconWithBg(lv_obj_t* canvas, IconId iconId, lv_color_t bgColor, lv_color_t fgColor) { +void drawIconWithBg(lv_obj_t* canvas, IconId iconId, lv_color_t bgColor, lv_color_t fgColor, int scale) { if (canvas == nullptr) return; const Icon& icon = getIcon(iconId); @@ -212,7 +247,6 @@ void drawIconWithBg(lv_obj_t* canvas, IconId iconId, lv_color_t bgColor, lv_colo lv_canvas_fill_bg(canvas, bgColor, LV_OPA_COVER); - const int scale = 2; const int width = icon.width; const int height = icon.height; @@ -232,8 +266,16 @@ void drawIconWithBg(lv_obj_t* canvas, IconId iconId, lv_color_t bgColor, lv_colo } } -void drawIcon(lv_obj_t* canvas, IconId iconId) { - drawIconWithBg(canvas, iconId, lv_color_hex(0x333333), lv_color_hex(0xFFFFFF)); +void drawIcon(lv_obj_t* canvas, IconId iconId, int scale) { + drawIconWithBg(canvas, iconId, lv_color_hex(0x333333), lv_color_hex(0xFFFFFF), scale); +} + +// Icon backing store: heap 24x24 slices on XXL, shared static 16x16 below that +lv_color_t* mainViewIconBuffer(MainViewState* state, int idx) { + if (state->heapIconBuf != nullptr) { + return &state->heapIconBuf[idx * 24 * 24]; + } + return TamaTac_iconBuffers[idx]; } void onAnimTimer(lv_timer_t* timer) { @@ -253,6 +295,18 @@ void onAnimTimer(lv_timer_t* timer) { return; } + // Skip the full canvas redraw when nothing would change: at 6-8x scale a + // redraw is tens of thousands of set_px calls, and most 200ms ticks land + // on an identical frame (drawSprite refreshes the cache when it draws). + const uint32_t now = tt::kernel::getMillis(); + const int frameIdx = currentFrameIdx(state->currentSpriteId, now, state->animStartTime); + const int phase = overlayPhase(state->currentSpriteId, state->currentDayPhase, now); + const bool flash = (state->evolutionFlashUntil > 0 && now < state->evolutionFlashUntil); + if (frameIdx == state->lastFrameIdx && phase == state->lastOverlayPhase && + flash == state->lastFlash && state->currentSpriteId == state->lastSpriteId) { + return; + } + const PetStats& stats = ctx->petLogic.getStats(); drawSprite(state, state->currentSpriteId, &stats); } @@ -316,19 +370,274 @@ void onPetTapped(lv_event_t* e) { tamaTacHandlePetTap(ctx); } +// Fill-to-fit: grow/shrink the integer pet scale so the column fills the +// wrapper's real content area (called on wrapper SIZE_CHANGED, so it tracks +// rotation/resurface too). Clamped to 2x..8x (8x = 192px, ~73KB heap while +// MainView is active). Small screens return early, keeping their compact +// tier layout untouched. +void mainViewRefitToFill(Context* ctx) { + MainViewState* state = &ctx->mainView; + if (!state->mainWrapper || !state->petStatsRow || !state->statsContainer || + !state->btnContainer || !state->statusLabel || !state->petArea || + !state->petCanvas || !state->poopContainer) { + return; + } + + int cw = lv_obj_get_content_width(state->mainWrapper); + int ch = lv_obj_get_content_height(state->mainWrapper); + if (cw < 120 || ch < 120) return; // not laid out yet + + int stH = lv_obj_get_height(state->statusLabel); + int btnH = lv_obj_get_height(state->btnContainer); + int poopHt = lv_obj_get_height(state->poopContainer); + if (stH <= 0 || btnH <= 0 || poopHt < 0) return; + + int fixed = stH + btnH + 2 * state->padRow + 2 * state->padMain; + int availRow = ch - fixed; + if (availRow < 40) return; + + int target = state->baseScale; + if (!state->stacked) { + int rowW = lv_obj_get_content_width(state->petStatsRow); + if (rowW < 120) return; + int sW = (rowW - state->framePad - 8 - 160) / 24; + int sH = (availRow - state->framePad - poopHt - state->padRow) / 24; + target = (sW < sH) ? sW : sH; + } else { + int statsH = lv_obj_get_height(state->statsContainer); + if (statsH <= 0) return; + int sH = (availRow - state->framePad - poopHt - state->padRow - statsH - state->padRow) / 24; + int sW = (cw - state->framePad) / 24; + target = (sW < sH) ? sW : sH; + } + + // No base floor: the tier is only the pre-layout estimate. If the real + // content area fits less (short-wide screens like the 640x320 simulator), + // shrink to fit; if it fits more, grow to fill. Small screens return + // early above, so their compact layout is untouched. + if (target < 2) target = 2; + if (target > 8) target = 8; + if (target == state->spriteScale) return; + + int newCanvas = target * 24; + lv_color_t* buf = TamaTac_canvasBuffer; + if (newCanvas > 72) { + // Allocate the replacement first and only then release the old buffer: + // a failed realloc would otherwise leave heapCanvas dangling (or force + // a wasteful copy). The old buffer stays live if this fails. + const size_t newBytes = static_cast(newCanvas) * static_cast(newCanvas) * sizeof(lv_color_t); + lv_color_t* fresh = static_cast(malloc(newBytes)); + if (fresh == nullptr) return; + if (state->heapCanvas != nullptr) { + free(state->heapCanvas); + } + state->heapCanvas = fresh; + buf = fresh; + } else if (state->heapCanvas != nullptr) { + // Shrinking back to the static buffer: point at it first, then free. + buf = TamaTac_canvasBuffer; + free(state->heapCanvas); + state->heapCanvas = nullptr; + } + + int newArea = newCanvas + state->framePad; + state->spriteScale = target; + state->petCanvasSize = newCanvas; + lv_obj_set_size(state->petArea, newArea, newArea); + lv_obj_set_size(state->petCanvas, newCanvas, newCanvas); + lv_canvas_set_buffer(state->petCanvas, buf, newCanvas, newCanvas, LV_COLOR_FORMAT_NATIVE); + lv_obj_center(state->petCanvas); + lv_obj_set_width(state->poopContainer, newArea); + if (!state->stacked) { + lv_obj_set_height(state->statsContainer, newArea); + } + mainViewUpdatePetDisplay(ctx); +} + +// Button row placement: explicit ROW containers, never ROW_WRAP (wrap has +// proven unreliable here: exact-fit rows collapse to a single column). Four +// across in one row, or 2+2 in two rows. Reparents only on mode change - +// re-setting the same parent would reshuffle the order. contW <= 0 means +// percent mode (small/medium/large tiers): full-width single row. +void mainViewPlaceButtons(Context* ctx, bool twoRows, int bw, int contW) { + MainViewState* state = &ctx->mainView; + if (!state->btnRowTop || !state->btnRowBottom || !state->btnContainer || + !state->feedBtn || !state->playBtn || !state->medicineBtn || !state->sleepBtn) { + return; + } + lv_obj_set_width(state->feedBtn, bw); + lv_obj_set_width(state->playBtn, bw); + lv_obj_set_width(state->medicineBtn, bw); + lv_obj_set_width(state->sleepBtn, bw); + if (contW <= 0) { + lv_obj_set_width(state->btnContainer, LV_PCT(100)); + lv_obj_set_width(state->btnRowTop, LV_PCT(100)); + } else { + lv_obj_set_width(state->btnContainer, contW); + } + if (twoRows == state->buttonsTwoRows) { + // Same mode: refresh widths in case the target changed + if (contW <= 0) { + lv_obj_set_width(state->btnContainer, LV_PCT(100)); + lv_obj_set_width(state->btnRowTop, LV_PCT(100)); + } else if (twoRows) { + // +16px slack: exact-fit rows clip a pixel under theme minimums + int rowW = 2 * bw + state->btnGap + 16; + lv_obj_set_width(state->btnRowTop, rowW); + lv_obj_set_width(state->btnRowBottom, rowW); + } else { + lv_obj_set_width(state->btnRowTop, contW); + } + return; + } + state->buttonsTwoRows = twoRows; + if (twoRows) { + int rowW = 2 * bw + state->btnGap + 16; + lv_obj_set_width(state->btnRowTop, rowW); + lv_obj_set_width(state->btnRowBottom, rowW); + lv_obj_set_parent(state->feedBtn, state->btnRowTop); + lv_obj_set_parent(state->playBtn, state->btnRowTop); + lv_obj_set_parent(state->medicineBtn, state->btnRowBottom); + lv_obj_set_parent(state->sleepBtn, state->btnRowBottom); + lv_obj_set_hidden(state->btnRowBottom, false); + } else { + if (contW > 0) { + lv_obj_set_width(state->btnRowTop, contW); + } + lv_obj_set_parent(state->feedBtn, state->btnRowTop); + lv_obj_set_parent(state->playBtn, state->btnRowTop); + lv_obj_set_parent(state->medicineBtn, state->btnRowTop); + lv_obj_set_parent(state->sleepBtn, state->btnRowTop); + lv_obj_set_hidden(state->btnRowBottom, true); + } +} +// Width fit: the create-time cap is display-based, but the real window can +// be narrower (portrait Tab5 system panels). Shrink the capped rows to the +// wrapper's actual content width. Percent-mode tiers follow the parent +// automatically and skip this. +// +// Button widths are MEASURED, not assumed: theme minimums can render buttons +// wider than set, and only the rendered width tells whether four fit. The +// layout timer re-runs this until it converges (set widths -> layout +// settles -> measure -> size container from reality). +void mainViewFitWidths(Context* ctx) { + MainViewState* state = &ctx->mainView; + if (!state->mainWrapper || !state->petStatsRow || !state->btnContainer || + !state->statusLabel || !state->feedBtn || !state->playBtn || + !state->medicineBtn || !state->sleepBtn) { + return; + } + if (state->contentPx <= 0) return; + + int cw = lv_obj_get_content_width(state->mainWrapper); + if (cw < 50) return; // not laid out yet + + // Live cap: recomputed from the CURRENT display resolution every pass. + // A create-time cap goes stale across IMU rotation (portrait 688 would + // pin landscape to 2x2 forever). Shrink to the real window when narrower, + // restore to the cap when wider again. Unchanged widths no-op in LVGL. + TamaViewport liveVp = tamaGetViewport(); + int liveCap = tamaContentCap(liveVp); + if (liveCap > 0) { + state->contentPx = liveCap; + } + int target = (cw < state->contentPx) ? cw : state->contentPx; + lv_obj_set_width(state->petStatsRow, target); + lv_obj_set_width(state->statusLabel, target); + + int gap = state->btnGap; + int measured = lv_obj_get_width(state->feedBtn); + if (measured < 10) measured = state->baseBtnW; // pre-layout fallback + + // Four across only with a full gap of breathing room past reality; + // otherwise explicit 2+2 rows (never wrap). + bool four = (4 * measured + 3 * gap <= target - gap); + int bw = state->baseBtnW; + if (four) { + int maxFour = (target - 4 * gap) / 4; + if (maxFour < bw) bw = (maxFour >= 48) ? maxFour : bw; + } + // +16px slack: exact-fit rows clip under theme minimums + int contW = four ? target : 2 * bw + gap + 16; + mainViewPlaceButtons(ctx, !four, bw, contW); +} + +void onMainWrapperResized(lv_event_t* e) { + auto* ctx = static_cast(lv_event_get_user_data(e)); + if (ctx == nullptr || ctx->activeView != ViewType::Main) return; + mainViewFitWidths(ctx); + mainViewRefitToFill(ctx); +} + +// Deferred layout passes: SIZE_CHANGED only fires when the wrapper's size +// *changes*, so on first show (sized right from the start) the corrections +// never run. Re-run fit+refit a few times until layout settles instead of +// trusting a single shot (window grant / first layout may lag creation). +void onLayoutTimer(lv_timer_t* timer) { + auto* ctx = static_cast(lv_timer_get_user_data(timer)); + MainViewState* state = &ctx->mainView; + state->layoutTimer = nullptr; + state->layoutPasses++; + if (ctx->activeView != ViewType::Main) return; // navigated away: stop + bool granted = (window_manager_get_state(ctx->window) == WINDOW_STATE_GRANTED); + if (granted) { + // Snapshot to detect a settled layout (snapshot BEFORE fit/refit) + const int scaleBefore = state->spriteScale; + const int btnWBefore = (state->btnContainer != nullptr) + ? lv_obj_get_width(state->btnContainer) : 0; + const int rowWBefore = (state->petStatsRow != nullptr) + ? lv_obj_get_width(state->petStatsRow) : 0; + mainViewFitWidths(ctx); + mainViewRefitToFill(ctx); + // Reveal: the wrapper starts invisible so the estimate->measured + // correction never shows as a resize morph, only as a pop-in. + lv_obj_set_style_opa(state->mainWrapper, LV_OPA_COVER, 0); + // Only a fully-laid-out pass counts as calm: both fit and refit bail + // early (changing nothing) while the wrapper is still unmeasured, and + // that must not look like convergence or the timer stops too soon. + const int contentH = (state->mainWrapper != nullptr) + ? lv_obj_get_content_height(state->mainWrapper) : 0; + const int contentW = (state->mainWrapper != nullptr) + ? lv_obj_get_content_width(state->mainWrapper) : 0; + if (contentW >= 120 && contentH >= 120 && + state->spriteScale == scaleBefore && + lv_obj_get_width(state->btnContainer) == btnWBefore && + lv_obj_get_width(state->petStatsRow) == rowWBefore) { + state->layoutCalm++; + } else { + state->layoutCalm = 0; + } + } + // Stop early once settled (usually pass 2); cap at 5 while the + // window/grant is still settling (launch can lag creation). + if (state->layoutCalm < 1 && state->layoutPasses < 5) { + state->layoutTimer = lv_timer_create(onLayoutTimer, 150, ctx); + lv_timer_set_repeat_count(state->layoutTimer, 1); + } +} + } // namespace +// Forward: scenery/background builders (defined with the update fns below) +void createScenery(lv_obj_t* parent, Context* ctx, const TamaViewport& vp); +void mainViewApplyBackground(Context* ctx); + void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { MainViewState* state = &ctx->mainView; - // Detect screen size for responsive layout - lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr); - lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr); - bool isSmall = (screenWidth < 280 || screenHeight < 180); - bool isXLarge = (screenWidth >= 600); - bool isLarge = !isXLarge && (screenWidth >= 400 && screenHeight >= 300); - - // Scaled dimensions based on screen tier + // Shared viewport detection (display resolution + orientation) + TamaViewport vp = tamaGetViewport(); + bool isSmall = tamaIsSmall(vp); + bool isXLarge = tamaIsXLarge(vp); + bool isXXLarge = tamaIsXXLarge(vp); + bool isLarge = !isXLarge && (vp.width >= 400 && vp.height >= 300); + // Portrait-narrow screens (e.g. 135x240) stack pet above stats; + // landscape and wide screens keep the side-by-side row. + bool stackVertical = vp.isPortrait && vp.isNarrow; + + // Scaled dimensions based on screen tier. + // Static 72x72 buffer covers Small/Medium; Large/XLarge heap-allocate + // so no-PSRAM devices never pay the extra RAM (see heapCanvas). int petAreaSize, statBarWidth, statBarHeight; int btnWidth, btnHeight, padMain, padRow, poopHeight; @@ -337,47 +646,137 @@ void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { statBarWidth = 85; statBarHeight = 5; btnWidth = 54; btnHeight = 32; padMain = 1; padRow = 1; poopHeight = 14; + state->iconPx = 16; + } else if (isXXLarge) { + petAreaSize = 168; state->petCanvasSize = 144; state->spriteScale = 6; // 24*6=144 + statBarWidth = 320; statBarHeight = 16; + btnWidth = 170; btnHeight = 76; + padMain = 16; padRow = 10; poopHeight = 26; + state->iconPx = 24; } else if (isXLarge) { - petAreaSize = 136; state->petCanvasSize = 72; state->spriteScale = 3; // 24*3=72 - statBarWidth = 300; statBarHeight = 10; - btnWidth = 120; btnHeight = 56; - padMain = 8; padRow = 6; poopHeight = 24; + petAreaSize = 168; state->petCanvasSize = 144; state->spriteScale = 6; // 24*6=144 + statBarWidth = 300; statBarHeight = 12; + btnWidth = 150; btnHeight = 64; + padMain = 12; padRow = 8; poopHeight = 24; + state->iconPx = 16; } else if (isLarge) { - petAreaSize = 100; state->petCanvasSize = 72; state->spriteScale = 3; + petAreaSize = 112; state->petCanvasSize = 96; state->spriteScale = 4; // 24*4=96 statBarWidth = 200; statBarHeight = 8; btnWidth = 90; btnHeight = 50; padMain = 4; padRow = 4; poopHeight = 22; + state->iconPx = 16; } else { // Medium (default) petAreaSize = 76; state->petCanvasSize = 72; state->spriteScale = 3; statBarWidth = 135; statBarHeight = 6; btnWidth = 70; btnHeight = 42; padMain = 1; padRow = 1; poopHeight = 16; + state->iconPx = 16; + } + + // Canvas backing store: shared static buffer up to 72px, heap beyond that + lv_color_t* canvasBuf = TamaTac_canvasBuffer; + if (state->petCanvasSize > 72) { + state->heapCanvas = static_cast( + malloc(static_cast(state->petCanvasSize) * static_cast(state->petCanvasSize) * sizeof(lv_color_t))); + if (state->heapCanvas != nullptr) { + canvasBuf = state->heapCanvas; + } else { + // OOM fallback: stay within the static buffer, keeping the tier's + // frame padding so petAreaSize/framePad stay consistent (a 168px + // area around a 72px canvas would misplace everything downstream). + int frame = petAreaSize - state->petCanvasSize; + state->petCanvasSize = 72; + state->spriteScale = 3; + petAreaSize = 72 + frame; + } } + // Icon backing store: 24x24 heap slices on XXL, shared static 16x16 below + if (state->iconPx > 16) { + state->heapIconBuf = static_cast( + malloc(12U * 24U * 24U * sizeof(lv_color_t))); + if (state->heapIconBuf == nullptr) { + state->iconPx = 16; // OOM fallback: small icons + } + } + const int iconScale = state->iconPx / 8; // 2 or 3 + state->baseScale = state->spriteScale; + state->framePad = petAreaSize - state->petCanvasSize; + state->padMain = padMain; + state->padRow = padRow; + state->poopH = poopHeight; + state->stacked = stackVertical; + + // Cap content width on big screens so bars/buttons don't stretch absurdly + // (e.g. 1280px Tab5 landscape -> 800px centered column, 800x480 -> 720px). + // Live recompute on every fit (rotation changes vp) - see fitWidths. + int contentWidthPct = 98; + int contentWidthPx = tamaContentCap(vp); + + // Button metrics. baseBtnW keeps the tier width so rotation back to + // landscape restores full size; placement happens after the buttons + // exist (mainViewPlaceButtons below) and re-runs post-layout. + state->btnGap = isSmall ? 3 : (isXLarge ? 16 : 6); + state->baseBtnW = btnWidth; + state->contentPx = contentWidthPx; + // Initialize animation state state->animStartTime = tt::kernel::getMillis(); state->currentSpriteId = SPRITE_EGG_IDLE; - - // Main content wrapper + // Fresh draw cache: stale values from a previous view lifetime would make + // the anim timer wrongly skip the first draws + state->lastFrameIdx = -1; + state->lastOverlayPhase = -1; + state->lastFlash = false; + state->lastSpriteId = SPRITE_EGG_IDLE; + + // Main content wrapper (vertically centered on big screens so the + // pet block doesn't cling to the toolbar leaving half the screen empty). + // Backdrop is a day/night vertical gradient (free fill, no image layer); + // scenery decor parents to the wrapper first so it paints behind content. + // bgApplied resets so fresh objects get styled on first update. + state->bgApplied = false; state->mainWrapper = lv_obj_create(parentWidget); lv_obj_set_size(state->mainWrapper, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_pad_all(state->mainWrapper, padMain, 0); - lv_obj_set_style_bg_opa(state->mainWrapper, LV_OPA_TRANSP, 0); + lv_obj_set_style_bg_opa(state->mainWrapper, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(state->mainWrapper, lv_color_hex(0x4760B8), LV_PART_MAIN); lv_obj_set_style_border_width(state->mainWrapper, 0, 0); lv_obj_set_flex_flow(state->mainWrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(state->mainWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_remove_flag(state->mainWrapper, LV_OBJ_FLAG_SCROLLABLE); + if (isXLarge) { + lv_obj_set_flex_align(state->mainWrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + } else { + lv_obj_set_flex_align(state->mainWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + } + lv_obj_set_scrollable(state->mainWrapper, false); + // Refit pet scale once the real content area is known (and on rotation) + lv_obj_add_event_cb(state->mainWrapper, onMainWrapperResized, LV_EVENT_SIZE_CHANGED, ctx); + + // Scenery first-children: sun/moon/clouds/ground paint above the gradient + // but below all content (XL/XXLarge only, decided inside) + createScenery(state->mainWrapper, ctx, vp); - // Pet and stats container (horizontal layout) + // Pet and stats container: side-by-side row normally, + // stacked column on portrait-narrow screens (135x240 etc.) lv_obj_t* petStatsRow = lv_obj_create(state->mainWrapper); - lv_obj_set_size(petStatsRow, LV_PCT(98), LV_SIZE_CONTENT); + state->petStatsRow = petStatsRow; + if (contentWidthPx > 0) { + lv_obj_set_size(petStatsRow, contentWidthPx, LV_SIZE_CONTENT); + } else { + lv_obj_set_size(petStatsRow, LV_PCT(contentWidthPct), LV_SIZE_CONTENT); + } lv_obj_set_style_bg_opa(petStatsRow, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(petStatsRow, 0, 0); lv_obj_set_style_pad_all(petStatsRow, 0, 0); lv_obj_set_style_pad_left(petStatsRow, isSmall ? 2 : (isXLarge ? 16 : 8), 0); - lv_obj_set_flex_flow(petStatsRow, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(petStatsRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER); + if (stackVertical) { + lv_obj_set_flex_flow(petStatsRow, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(petStatsRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + } else { + lv_obj_set_flex_flow(petStatsRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(petStatsRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER); + } // Pet column (left side) lv_obj_t* petColumn = lv_obj_create(petStatsRow); @@ -398,12 +797,13 @@ void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_style_pad_all(state->petArea, 0, 0); // Make pet area tappable for pet interaction - lv_obj_add_flag(state->petArea, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_hidden(state->petArea, false); lv_obj_add_event_cb(state->petArea, onPetTapped, LV_EVENT_CLICKED, ctx); // Create canvas for sprite rendering state->petCanvas = lv_canvas_create(state->petArea); - lv_canvas_set_buffer(state->petCanvas, TamaTac_canvasBuffer, state->petCanvasSize, state->petCanvasSize, LV_COLOR_FORMAT_NATIVE); + lv_obj_set_size(state->petCanvas, state->petCanvasSize, state->petCanvasSize); + lv_canvas_set_buffer(state->petCanvas, canvasBuf, state->petCanvasSize, state->petCanvasSize, LV_COLOR_FORMAT_NATIVE); lv_obj_center(state->petCanvas); // Poop container @@ -415,18 +815,23 @@ void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_style_pad_column(state->poopContainer, isSmall ? 1 : (isXLarge ? 4 : 2), 0); lv_obj_set_flex_flow(state->poopContainer, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(state->poopContainer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_remove_flag(state->poopContainer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(state->poopContainer, false); // Create 3 poop icon canvases for (int i = 0; i < 3; i++) { state->poopIcons[i] = lv_canvas_create(state->poopContainer); - lv_canvas_set_buffer(state->poopIcons[i], TamaTac_iconBuffers[i], 16, 16, LV_COLOR_FORMAT_NATIVE); - lv_obj_add_flag(state->poopIcons[i], LV_OBJ_FLAG_HIDDEN); + lv_canvas_set_buffer(state->poopIcons[i], mainViewIconBuffer(state, i), state->iconPx, state->iconPx, LV_COLOR_FORMAT_NATIVE); + lv_obj_set_hidden(state->poopIcons[i], true); } - // Stat bars container + // Stat bars container (grows to fill remaining space in both orientations) lv_obj_t* statsContainer = lv_obj_create(petStatsRow); - lv_obj_set_size(statsContainer, LV_SIZE_CONTENT, petAreaSize); + state->statsContainer = statsContainer; + if (stackVertical) { + lv_obj_set_size(statsContainer, LV_PCT(100), LV_SIZE_CONTENT); + } else { + lv_obj_set_size(statsContainer, LV_SIZE_CONTENT, petAreaSize); + } lv_obj_set_style_bg_opa(statsContainer, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(statsContainer, 0, 0); lv_obj_set_style_pad_all(statsContainer, padRow, 0); @@ -435,13 +840,13 @@ void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_flex_flow(statsContainer, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(statsContainer, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); lv_obj_set_flex_grow(statsContainer, 1); - lv_obj_remove_flag(statsContainer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(statsContainer, false); - // Helper to create stat row + // Helper to create stat row (bar flex-grows; statBarWidth is the minimum) auto createStatRow = [&](lv_obj_t* parent, int iconBufferIdx, IconId iconId, lv_obj_t** bar, lv_color_t barColor) { lv_obj_t* row = lv_obj_create(parent); - lv_obj_set_size(row, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(row, 0, 0); lv_obj_set_style_pad_all(row, 0, 0); @@ -450,11 +855,12 @@ void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_t* iconCanvas = lv_canvas_create(row); - lv_canvas_set_buffer(iconCanvas, TamaTac_iconBuffers[iconBufferIdx], 16, 16, LV_COLOR_FORMAT_NATIVE); - drawIcon(iconCanvas, iconId); + lv_canvas_set_buffer(iconCanvas, mainViewIconBuffer(state, iconBufferIdx), state->iconPx, state->iconPx, LV_COLOR_FORMAT_NATIVE); + drawIcon(iconCanvas, iconId, iconScale); *bar = lv_bar_create(row); lv_obj_set_size(*bar, statBarWidth, statBarHeight); + lv_obj_set_flex_grow(*bar, 1); lv_bar_set_range(*bar, 0, 100); lv_obj_set_style_bg_color(*bar, lv_color_hex(0x444444), LV_PART_MAIN); lv_obj_set_style_bg_color(*bar, barColor, LV_PART_INDICATOR); @@ -467,30 +873,58 @@ void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { state->statIcons[2] = createStatRow(statsContainer, 5, ICON_HEALTH, &state->healthBar, lv_color_hex(0x00FF00)); state->statIcons[3] = createStatRow(statsContainer, 6, ICON_ENERGY, &state->energyBar, lv_color_hex(0x00CCFF)); - // Status label + // Status label (wraps on narrow screens instead of overflowing) state->statusLabel = lv_label_create(state->mainWrapper); lv_label_set_text(state->statusLabel, "Your pet is ready!"); + if (contentWidthPx > 0) { + lv_obj_set_width(state->statusLabel, contentWidthPx); + } else { + lv_obj_set_width(state->statusLabel, LV_PCT(100)); + } + lv_label_set_long_mode(state->statusLabel, LV_LABEL_LONG_WRAP); lv_obj_set_style_text_align(state->statusLabel, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_pad_top(state->statusLabel, 0, 0); lv_obj_set_style_pad_bottom(state->statusLabel, 0, 0); - // Button container + // Button container: explicit rows, never wrap (see mainViewPlaceButtons) lv_obj_t* btnContainer = lv_obj_create(state->mainWrapper); - lv_obj_set_width(btnContainer, LV_PCT(100)); + state->btnContainer = btnContainer; + // Width is set by mainViewPlaceButtons below lv_obj_set_height(btnContainer, LV_SIZE_CONTENT); lv_obj_set_style_bg_opa(btnContainer, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(btnContainer, 0, 0); lv_obj_set_style_pad_all(btnContainer, 0, 0); - lv_obj_set_style_pad_column(btnContainer, isSmall ? 3 : (isXLarge ? 16 : 6), 0); + lv_obj_set_style_pad_row(btnContainer, isSmall ? 3 : (isXLarge ? 16 : 6), 0); lv_obj_set_style_pad_top(btnContainer, padRow, 0); - lv_obj_set_flex_flow(btnContainer, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_flow(btnContainer, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(btnContainer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_align(btnContainer, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_set_scrollable(btnContainer, false); + + auto createBtnRow = [&](lv_obj_t** row) { + *row = lv_obj_create(btnContainer); + lv_obj_set_size(*row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(*row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(*row, 0, 0); + lv_obj_set_style_pad_all(*row, 0, 0); + lv_obj_set_style_pad_column(*row, isSmall ? 3 : (isXLarge ? 16 : 6), 0); + lv_obj_set_flex_flow(*row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(*row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_scrollable(*row, false); + }; + createBtnRow(&state->btnRowTop); + createBtnRow(&state->btnRowBottom); + lv_obj_set_hidden(state->btnRowBottom, true); // shown in 2x2 mode + state->buttonsTwoRows = false; - // Helper to create action button with icon + // Helper to create action button with icon (all start in the top row; + // mainViewPlaceButtons moves pairs down for 2x2 after measuring) auto createActionBtn = [&](lv_obj_t** btn, const char* text, IconId iconId, int bufferIdx, lv_event_cb_t callback) { - *btn = lv_btn_create(btnContainer); + *btn = lv_btn_create(state->btnRowTop); lv_obj_set_size(*btn, btnWidth, btnHeight); + // Our explicit widths must hold exactly: neutralize any theme button + // min-width, otherwise rows compute against a wider button than set + // and wrap unpredictably (portrait 3+1 / 1-column symptoms). + lv_obj_set_style_min_width(*btn, 0, LV_PART_MAIN); lv_obj_set_flex_flow(*btn, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(*btn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(*btn, isSmall ? 1 : (isXLarge ? 4 : 2), 0); @@ -501,11 +935,14 @@ void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { // Icon canvas with button background color lv_obj_t* iconCanvas = lv_canvas_create(*btn); - lv_canvas_set_buffer(iconCanvas, TamaTac_iconBuffers[bufferIdx], 16, 16, LV_COLOR_FORMAT_NATIVE); - drawIconWithBg(iconCanvas, iconId, btnBgColor, lv_color_hex(0xFFFFFF)); - - // Label - if (!isSmall) { + lv_canvas_set_buffer(iconCanvas, mainViewIconBuffer(state, bufferIdx), state->iconPx, state->iconPx, LV_COLOR_FORMAT_NATIVE); + drawIconWithBg(iconCanvas, iconId, btnBgColor, lv_color_hex(0xFFFFFF), iconScale); + + // Label (hidden only on short landscape-small screens like 240x135 + // where vertical space is critical; kept in portrait-narrow like + // 135x240 where buttons wrap 2x2 and height allows it) + bool showLabel = !(isSmall && vp.isShort && !stackVertical); + if (showLabel) { lv_obj_t* label = lv_label_create(*btn); lv_label_set_text(label, text); lv_obj_set_style_text_font(label, lv_font_get_default(), 0); @@ -519,11 +956,31 @@ void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { createActionBtn(&state->medicineBtn, "Med", ICON_MEDICINE, 9, onMedicineClicked); createActionBtn(&state->sleepBtn, "Sleep", ICON_SLEEP, 10, onSleepClicked); + // Initial button placement (display-based estimate; fitWidths corrects + // post-layout from measured widths) + if (contentWidthPx > 0) { + bool two = (4 * btnWidth + 3 * state->btnGap > contentWidthPx - state->btnGap); + int initCont = two ? 2 * btnWidth + state->btnGap + 16 : contentWidthPx; + mainViewPlaceButtons(ctx, two, btnWidth, initCont); + } else { + mainViewPlaceButtons(ctx, false, btnWidth, 0); + } + // Refresh timer for periodic UI updates (day/night, random events, etc.) state->refreshTimer = lv_timer_create(onRefreshTimer, 5000, ctx); // Animation timer (~5fps for smooth frame cycling) state->animTimer = lv_timer_create(onAnimTimer, 200, ctx); + + // Deferred layout correction (see onLayoutTimer). The wrapper starts + // fully transparent so the estimate->measured correction appears as a + // single pop-in rather than a visible resize morph; the first granted + // pass restores opacity. Transparency inherits (layout still runs). + lv_obj_set_style_opa(state->mainWrapper, LV_OPA_TRANSP, 0); + state->layoutPasses = 0; + state->layoutCalm = 0; + state->layoutTimer = lv_timer_create(onLayoutTimer, 100, ctx); + lv_timer_set_repeat_count(state->layoutTimer, 1); } void mainViewStop(Context* ctx) { @@ -536,7 +993,31 @@ void mainViewStop(Context* ctx) { lv_timer_del(state->animTimer); state->animTimer = nullptr; } + if (state->layoutTimer) { + lv_timer_del(state->layoutTimer); + state->layoutTimer = nullptr; + } + if (state->heapCanvas) { + free(state->heapCanvas); + state->heapCanvas = nullptr; + } + if (state->heapIconBuf) { + free(state->heapIconBuf); + state->heapIconBuf = nullptr; + } state->mainWrapper = nullptr; + state->petStatsRow = nullptr; + state->statsContainer = nullptr; + state->btnContainer = nullptr; + state->btnRowTop = nullptr; + state->btnRowBottom = nullptr; + state->buttonsTwoRows = false; + state->bgStrip = nullptr; + state->bgSun = nullptr; + state->bgMoon = nullptr; + state->bgCloud1 = nullptr; + state->bgCloud2 = nullptr; + state->bgCloud3 = nullptr; state->statusLabel = nullptr; state->petArea = nullptr; state->petCanvas = nullptr; @@ -572,6 +1053,96 @@ void mainViewUpdateStatBars(Context* ctx) { if (state->energyBar) lv_bar_set_value(state->energyBar, stats.energy, LV_ANIM_OFF); } +// Scenery overlays (XL/XXL only): decor behind the content. +// All floating+anchored, so rotation tracks with zero resize code and the +// flex column math is untouched. Created before mainWrapper => behind it. +void createScenery(lv_obj_t* parent, Context* ctx, const TamaViewport& vp) { + MainViewState* state = &ctx->mainView; + + // NOTE: parent is mainWrapper itself, and these are its FIRST children so + // they paint above the wrapper background but below all content. All are + // floating (out of the flex flow) and anchored (track resizes for free). + + // Sun/moon/clouds/ground need room to breathe: XL/XXL only + if (!tamaIsXLarge(vp)) return; + auto makeOrb = [&](lv_obj_t** out, const lv_image_dsc_t* dsc, + lv_color_t tint, lv_opa_t fade) { + *out = lv_image_create(parent); + lv_image_set_src(*out, dsc); + lv_obj_set_style_image_recolor(*out, tint, LV_PART_MAIN); + lv_obj_set_style_image_recolor_opa(*out, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_image_opa(*out, fade, LV_PART_MAIN); + lv_obj_set_floating(*out, true); + lv_obj_set_clickable(*out, false); + }; + + // Ground strip: full-bleed tiled grass pinned to the bottom + state->bgStrip = lv_obj_create(parent); + lv_obj_set_size(state->bgStrip, LV_PCT(100), 24); + lv_obj_set_style_pad_all(state->bgStrip, 0, 0); + lv_obj_set_style_border_width(state->bgStrip, 0, 0); + lv_obj_set_style_radius(state->bgStrip, 0, 0); + lv_obj_set_style_bg_image_src(state->bgStrip, &grassDayDsc, LV_PART_MAIN); + lv_obj_set_style_bg_image_tiled(state->bgStrip, true, LV_PART_MAIN); + lv_obj_set_style_bg_image_opa(state->bgStrip, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_floating(state->bgStrip, true); + lv_obj_set_clickable(state->bgStrip, false); + lv_obj_set_scrollable(state->bgStrip, false); + lv_obj_align(state->bgStrip, LV_ALIGN_BOTTOM_MID, 0, 0); + + makeOrb(&state->bgSun, &decorSunDsc, lv_color_hex(0xFFD94A), 200); + lv_obj_align(state->bgSun, LV_ALIGN_TOP_RIGHT, -20, 20); + + makeOrb(&state->bgMoon, &decorMoonDsc, lv_color_hex(0xE8E8F5), 200); + lv_obj_align(state->bgMoon, LV_ALIGN_TOP_RIGHT, -20, 20); + lv_obj_set_hidden(state->bgMoon, true); // day default; apply fixes + + makeOrb(&state->bgCloud1, &cloudBigDsc, lv_color_hex(0xFFFFFF), 110); + lv_obj_align(state->bgCloud1, LV_ALIGN_TOP_LEFT, + vp.width * 5 / 100, vp.height * 12 / 100); + + makeOrb(&state->bgCloud2, &cloudSmallDsc, lv_color_hex(0xFFFFFF), 110); + lv_obj_align(state->bgCloud2, LV_ALIGN_TOP_LEFT, + vp.width * 40 / 100, vp.height * 6 / 100); + + makeOrb(&state->bgCloud3, &cloudSmallDsc, lv_color_hex(0xFFFFFF), 90); + lv_obj_align(state->bgCloud3, LV_ALIGN_TOP_LEFT, + vp.width * 72 / 100, vp.height * 16 / 100); +} + +// Day/night backdrop: flat colors (RGB565 gradients band visibly and LVGL +// has no dither pass; flat is fastest and band-free) plus the XL/XXL scenery +// set. Swapped only on flips. +void mainViewApplyBackground(Context* ctx) { + MainViewState* state = &ctx->mainView; + if (state->mainWrapper == nullptr) return; + bool night = (state->currentDayPhase == DayPhase::Night); + if (state->bgApplied && night == state->bgIsNight) return; + lv_obj_set_style_bg_color(state->mainWrapper, + lv_color_hex(night ? 0x121839 : 0x4760B8), LV_PART_MAIN); + if (state->bgStrip != nullptr) { + lv_obj_set_style_bg_image_src(state->bgStrip, + night ? &grassNightDsc : &grassDayDsc, LV_PART_MAIN); + } + if (state->bgSun != nullptr) { + if (night) { + lv_obj_set_hidden(state->bgSun, true); + lv_obj_set_hidden(state->bgCloud1, true); + lv_obj_set_hidden(state->bgCloud2, true); + lv_obj_set_hidden(state->bgCloud3, true); + lv_obj_set_hidden(state->bgMoon, false); + } else { + lv_obj_set_hidden(state->bgSun, false); + lv_obj_set_hidden(state->bgCloud1, false); + lv_obj_set_hidden(state->bgCloud2, false); + lv_obj_set_hidden(state->bgCloud3, false); + lv_obj_set_hidden(state->bgMoon, true); + } + } + state->bgIsNight = night; + state->bgApplied = true; +} + void mainViewUpdatePetDisplay(Context* ctx) { MainViewState* state = &ctx->mainView; const PetStats& stats = ctx->petLogic.getStats(); @@ -599,6 +1170,7 @@ void mainViewUpdatePetDisplay(Context* ctx) { lv_color_t bg = (phase == DayPhase::Night) ? lv_color_hex(0x1a1a2e) : lv_color_hex(0x333333); lv_obj_set_style_bg_color(state->petArea, bg, 0); } + mainViewApplyBackground(ctx); // Update current sprite (animation timer will handle actual drawing) SpriteId newSprite = getSpriteForCurrentState(stats); @@ -612,10 +1184,10 @@ void mainViewUpdatePetDisplay(Context* ctx) { for (int i = 0; i < 3; i++) { if (state->poopIcons[i]) { if (i < stats.poopCount) { - drawIcon(state->poopIcons[i], ICON_POOP); - lv_obj_clear_flag(state->poopIcons[i], LV_OBJ_FLAG_HIDDEN); + drawIcon(state->poopIcons[i], ICON_POOP, state->iconPx / 8); + lv_obj_set_hidden(state->poopIcons[i], false); } else { - lv_obj_add_flag(state->poopIcons[i], LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(state->poopIcons[i], true); } } } diff --git a/Apps/TamaTac/main/Source/MainView.h b/Apps/TamaTac/main/Source/MainView.h index 6b44f4c..1c76623 100644 --- a/Apps/TamaTac/main/Source/MainView.h +++ b/Apps/TamaTac/main/Source/MainView.h @@ -13,6 +13,12 @@ struct Context; struct MainViewState { // UI elements lv_obj_t* mainWrapper = nullptr; + lv_obj_t* petStatsRow = nullptr; + lv_obj_t* statsContainer = nullptr; + lv_obj_t* btnContainer = nullptr; + lv_obj_t* btnRowTop = nullptr; + lv_obj_t* btnRowBottom = nullptr; + bool buttonsTwoRows = false; lv_obj_t* statusLabel = nullptr; lv_obj_t* petArea = nullptr; lv_obj_t* petCanvas = nullptr; @@ -34,6 +40,17 @@ struct MainViewState { // Animation lv_timer_t* animTimer = nullptr; + // One-shot deferred layout correction (deleted after firing / in stop) + lv_timer_t* layoutTimer = nullptr; + // Deferred layout pass counters (state survives rebuilds: reset at create) + int layoutPasses = 0; + int layoutCalm = 0; + // Last-drawn frame cache: the 200ms anim timer skips the full canvas + // redraw when neither the sprite frame nor an overlay phase changed + int lastFrameIdx = -1; + int lastOverlayPhase = -1; + bool lastFlash = false; + SpriteId lastSpriteId = SPRITE_EGG_IDLE; uint32_t animStartTime = 0; SpriteId currentSpriteId = SPRITE_EGG_IDLE; @@ -44,6 +61,38 @@ struct MainViewState { // Scaling int spriteScale = 3; int petCanvasSize = 72; + // Heap canvas for scales above the shared 72x72 static buffer + // (Large 96px / XLarge 144px, plus fill-to-fit growth up to 192px). + // Null when the static buffer is used, so small no-PSRAM devices + // never pay the extra RAM. + lv_color_t* heapCanvas = nullptr; + // Heap icon block (12 x 24x24) for the XXL 3x icons. Null otherwise, + // so only Tab5-class devices pay the extra ~14KB while MainView runs. + lv_color_t* heapIconBuf = nullptr; + // Icon canvas edge in px (16, or 24 on XXL) and its pixel-doubling scale. + int iconPx = 16; + // Tiled background day/night state + bool bgIsNight = false; + bool bgApplied = false; + // Scenery overlays (XL/XXL only, behind content) + lv_obj_t* bgStrip = nullptr; + lv_obj_t* bgSun = nullptr; + lv_obj_t* bgMoon = nullptr; + lv_obj_t* bgCloud1 = nullptr; + lv_obj_t* bgCloud2 = nullptr; + lv_obj_t* bgCloud3 = nullptr; + // Fill-to-fit bookkeeping (tier base + layout metrics for refit) + int padMain = 0; + int padRow = 0; + int poopH = 0; + int framePad = 0; // petAreaSize - petCanvasSize at tier base + int baseScale = 3; + bool stacked = false; + // Width bookkeeping: fixed cap applied at create (0 = percent mode), + // button base width + gap for post-layout shrinking to the real window. + int contentPx = 0; + int baseBtnW = 0; + int btnGap = 6; }; void mainViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx); diff --git a/Apps/TamaTac/main/Source/MenuView.cpp b/Apps/TamaTac/main/Source/MenuView.cpp index e482386..13ad49f 100644 --- a/Apps/TamaTac/main/Source/MenuView.cpp +++ b/Apps/TamaTac/main/Source/MenuView.cpp @@ -4,6 +4,7 @@ */ #include "MenuView.h" +#include "Layout.h" #include "TamaTac.h" namespace { @@ -42,12 +43,10 @@ lv_obj_t* addStyledListBtn(Context* ctx, lv_obj_t* menuList, const char* icon, c void menuViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { MenuViewState* state = &ctx->menuView; - // Detect screen size for responsive layout - // Use display resolution for reliable sizing (parent may not be laid out yet on first load) - lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr); - lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr); - bool isSmall = (screenWidth < 280 || screenHeight < 180); - bool isXLarge = (screenWidth >= 600); + // Shared viewport detection (display resolution + orientation) + TamaViewport vp = tamaGetViewport(); + bool isSmall = tamaIsSmall(vp); + bool isXLarge = tamaIsXLarge(vp); // Main wrapper state->mainWrapper = lv_obj_create(parentWidget); @@ -57,7 +56,7 @@ void menuViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_style_border_width(state->mainWrapper, 0, 0); lv_obj_set_flex_flow(state->mainWrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(state->mainWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_remove_flag(state->mainWrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(state->mainWrapper, false); // Menu list state->menuList = lv_list_create(state->mainWrapper); diff --git a/Apps/TamaTac/main/Source/PatternGame.cpp b/Apps/TamaTac/main/Source/PatternGame.cpp index 2dbcf9a..f85bc64 100644 --- a/Apps/TamaTac/main/Source/PatternGame.cpp +++ b/Apps/TamaTac/main/Source/PatternGame.cpp @@ -4,6 +4,7 @@ */ #include "PatternGame.h" +#include "Layout.h" #include "TamaTac.h" #include "SfxEngine.h" #include @@ -244,12 +245,11 @@ void onButtonClicked(lv_event_t* e) { void patternGameCreateWidgets(lv_obj_t* parent, Context* ctx) { PatternGameState* state = &ctx->patternGame; - // Screen size detection - lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr); - lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr); - bool isSmall = (screenWidth < 280 || screenHeight < 180); - bool isXLarge = (screenWidth >= 600); - bool isLarge = !isXLarge && (screenWidth >= 400 && screenHeight >= 300); + // Screen size detection (shared helper) + TamaViewport vp = tamaGetViewport(); + bool isSmall = tamaIsSmall(vp); + bool isXLarge = tamaIsXLarge(vp); + bool isLarge = !isXLarge && (vp.width >= 400 && vp.height >= 300); // Scaled dimensions int btnSize = isSmall ? 50 : (isXLarge ? 120 : (isLarge ? 90 : 70)); @@ -257,6 +257,16 @@ void patternGameCreateWidgets(lv_obj_t* parent, Context* ctx) { int pad = isSmall ? 4 : (isXLarge ? 16 : 8); int radius = isSmall ? 6 : (isXLarge ? 16 : 10); + // Fill sizing: grow the 2x2 grid toward the display's short side, + // shrink it into short landscape screens (240x135). Floor 36px. + { + int avail = vp.width < vp.height ? vp.width : vp.height; + int fillBtn = (avail - 100 - gap * 3) / 2; + if (fillBtn < 32) fillBtn = 32; + if (fillBtn > 170) fillBtn = 170; + btnSize = fillBtn; + } + // Main wrapper lv_obj_t* wrapper = lv_obj_create(parent); lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100)); @@ -266,14 +276,15 @@ void patternGameCreateWidgets(lv_obj_t* parent, Context* ctx) { lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_row(wrapper, gap, 0); - lv_obj_remove_flag(wrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(wrapper, false); // Status label state->statusLabel = lv_label_create(wrapper); lv_label_set_text(state->statusLabel, "Get ready..."); lv_obj_set_style_text_align(state->statusLabel, LV_TEXT_ALIGN_CENTER, 0); - // 2x2 button grid + // 2x2 button grid: two explicit rows (a WRAP grid exact-fits its content + // width and can collapse to one clipped column on rounding) lv_obj_t* grid = lv_obj_create(wrapper); int gridSize = btnSize * 2 + gap * 3; lv_obj_set_size(grid, gridSize, gridSize); @@ -283,20 +294,33 @@ void patternGameCreateWidgets(lv_obj_t* parent, Context* ctx) { lv_obj_set_style_bg_color(grid, lv_color_hex(0x1a1a2e), 0); lv_obj_set_style_border_width(grid, 0, 0); lv_obj_set_style_radius(grid, radius, 0); - lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP); + lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_remove_flag(grid, LV_OBJ_FLAG_SCROLLABLE); - - // Create 4 game buttons - for (int i = 0; i < 4; i++) { - state->buttons[i] = lv_btn_create(grid); - lv_obj_set_size(state->buttons[i], btnSize, btnSize); - lv_obj_set_style_bg_color(state->buttons[i], lv_color_hex(DIM_COLORS[i]), 0); - lv_obj_set_style_bg_color(state->buttons[i], lv_color_hex(BRIGHT_COLORS[i]), LV_STATE_PRESSED); - lv_obj_set_style_radius(state->buttons[i], radius, 0); - lv_obj_set_style_border_width(state->buttons[i], 0, 0); - lv_obj_set_style_shadow_width(state->buttons[i], 0, 0); - lv_obj_add_event_cb(state->buttons[i], onButtonClicked, LV_EVENT_CLICKED, ctx); + lv_obj_set_scrollable(grid, false); + + // Create 4 game buttons in 2 fixed rows + for (int r = 0; r < 2; r++) { + lv_obj_t* row = lv_obj_create(grid); + lv_obj_set_size(row, btnSize * 2 + gap, btnSize); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 0, 0); + lv_obj_set_style_pad_column(row, gap, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_scrollable(row, false); + + for (int c = 0; c < 2; c++) { + int i = r * 2 + c; + state->buttons[i] = lv_btn_create(row); + lv_obj_set_size(state->buttons[i], btnSize, btnSize); + lv_obj_set_style_bg_color(state->buttons[i], lv_color_hex(DIM_COLORS[i]), 0); + lv_obj_set_style_bg_color(state->buttons[i], lv_color_hex(BRIGHT_COLORS[i]), LV_STATE_PRESSED); + lv_obj_set_style_radius(state->buttons[i], radius, 0); + lv_obj_set_style_border_width(state->buttons[i], 0, 0); + lv_obj_set_style_shadow_width(state->buttons[i], 0, 0); + lv_obj_add_event_cb(state->buttons[i], onButtonClicked, LV_EVENT_CLICKED, ctx); + } } // Initialize game diff --git a/Apps/TamaTac/main/Source/PetStats.h b/Apps/TamaTac/main/Source/PetStats.h index 58c5b51..4f88c0d 100644 --- a/Apps/TamaTac/main/Source/PetStats.h +++ b/Apps/TamaTac/main/Source/PetStats.h @@ -89,7 +89,7 @@ struct PetStats { LifeStage stage; AnimState currentAnim; Personality personality; - RandomEvent lastEvent; // Transient — not saved/loaded + RandomEvent lastEvent; // Transient - not saved/loaded // Timestamps uint32_t lastUpdateTime; // Last stat update (millis) diff --git a/Apps/TamaTac/main/Source/ReactionGame.cpp b/Apps/TamaTac/main/Source/ReactionGame.cpp index 269dfe2..e4c61ee 100644 --- a/Apps/TamaTac/main/Source/ReactionGame.cpp +++ b/Apps/TamaTac/main/Source/ReactionGame.cpp @@ -4,6 +4,7 @@ */ #include "ReactionGame.h" +#include "Layout.h" #include "TamaTac.h" #include "SfxEngine.h" #include @@ -30,7 +31,7 @@ void scheduleTimer(Context* ctx, uint32_t ms); void startRound(Context* ctx) { ReactionGameState* state = &ctx->reactionGame; state->phase = ReactionGameState::Phase::WaitForTarget; - if (state->targetBtn) lv_obj_add_flag(state->targetBtn, LV_OBJ_FLAG_HIDDEN); + if (state->targetBtn) lv_obj_set_hidden(state->targetBtn, true); char msg[48]; snprintf(msg, sizeof(msg), "Round %d/%d - Wait...", state->round + 1, REACTION_GAME_MAX_ROUNDS); @@ -46,7 +47,7 @@ void showTarget(Context* ctx) { state->phase = ReactionGameState::Phase::TargetShown; state->targetShowTime = tt::kernel::getMillis(); - if (state->targetBtn) lv_obj_clear_flag(state->targetBtn, LV_OBJ_FLAG_HIDDEN); + if (state->targetBtn) lv_obj_set_hidden(state->targetBtn, false); if (state->statusLabel) lv_label_set_text(state->statusLabel, "TAP NOW!"); if (ctx->sfxEngine) ctx->sfxEngine->play(SfxId::Blip); @@ -57,13 +58,13 @@ void showTarget(Context* ctx) { void returnToMain(Context* ctx) { ReactionGameState* state = &ctx->reactionGame; - tamaTacOnReactionGameComplete(ctx, state->score, state->score >= REACTION_GAME_MAX_ROUNDS); + tamaTacOnReactionGameComplete(ctx, state->score, state->fastHits >= REACTION_GAME_MAX_ROUNDS); } void showFinalResult(Context* ctx) { ReactionGameState* state = &ctx->reactionGame; char msg[64]; - if (state->score >= REACTION_GAME_MAX_ROUNDS) { + if (state->fastHits >= REACTION_GAME_MAX_ROUNDS) { snprintf(msg, sizeof(msg), LV_SYMBOL_OK " Perfect! %d/%d", state->score, REACTION_GAME_MAX_ROUNDS); } else if (state->score > 0) { snprintf(msg, sizeof(msg), "Score: %d/%d", state->score, REACTION_GAME_MAX_ROUNDS); @@ -81,14 +82,18 @@ void showFinalResult(Context* ctx) { void handleTap(Context* ctx) { ReactionGameState* state = &ctx->reactionGame; uint32_t reactionTime = tt::kernel::getMillis() - state->targetShowTime; - if (state->targetBtn) lv_obj_add_flag(state->targetBtn, LV_OBJ_FLAG_HIDDEN); + if (state->targetBtn) lv_obj_set_hidden(state->targetBtn, true); + // Completion and perfection are tracked separately: any in-time tap + // completes the round (score), only fast taps count toward perfect. + // Previously a single Slow tap made a perfect game unwinnable. + state->score++; const char* rating; if (reactionTime <= GREAT_TIME_MS) { - state->score++; + state->fastHits++; rating = "GREAT"; } else if (reactionTime <= GOOD_TIME_MS) { - state->score++; + state->fastHits++; rating = "Good"; } else { rating = "Slow"; @@ -133,7 +138,7 @@ void onTimerTick(lv_timer_t* timer) { break; case ReactionGameState::Phase::TargetShown: // Timeout - player didn't tap in time - if (state->targetBtn) lv_obj_add_flag(state->targetBtn, LV_OBJ_FLAG_HIDDEN); + if (state->targetBtn) lv_obj_set_hidden(state->targetBtn, true); if (state->statusLabel) lv_label_set_text(state->statusLabel, "Too slow!"); state->round++; state->phase = ReactionGameState::Phase::RoundResult; @@ -177,14 +182,23 @@ void onAreaClicked(lv_event_t* e) { void reactionGameCreateWidgets(lv_obj_t* parent, Context* ctx) { ReactionGameState* state = &ctx->reactionGame; - lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr); - lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr); - bool isSmall = (screenWidth < 280 || screenHeight < 180); - bool isXLarge = (screenWidth >= 600); + TamaViewport vp = tamaGetViewport(); + bool isSmall = tamaIsSmall(vp); + bool isXLarge = tamaIsXLarge(vp); int pad = isSmall ? 4 : (isXLarge ? 16 : 8); int targetSize = isSmall ? 80 : (isXLarge ? 200 : 120); + // Fill sizing: grow the target toward the display's short side (Tab5 was + // stuck at the old 200px), shrink into short displays. Floor 64px. + { + int avail = vp.width < vp.height ? vp.width : vp.height; + int fillTarget = avail - 120; + if (fillTarget < 64) fillTarget = 64; + if (fillTarget > 300) fillTarget = 300; + targetSize = fillTarget; + } + // Main wrapper (tappable for early-tap detection) lv_obj_t* wrapper = lv_obj_create(parent); lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100)); @@ -194,8 +208,8 @@ void reactionGameCreateWidgets(lv_obj_t* parent, Context* ctx) { lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_row(wrapper, pad, 0); - lv_obj_remove_flag(wrapper, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_add_flag(wrapper, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_scrollable(wrapper, false); + lv_obj_set_clickable(wrapper, true); lv_obj_add_event_cb(wrapper, onAreaClicked, LV_EVENT_CLICKED, ctx); // Status label @@ -210,7 +224,7 @@ void reactionGameCreateWidgets(lv_obj_t* parent, Context* ctx) { lv_obj_set_style_radius(state->targetBtn, targetSize / 2, 0); lv_obj_set_style_border_width(state->targetBtn, 0, 0); lv_obj_set_style_shadow_width(state->targetBtn, 0, 0); - lv_obj_add_flag(state->targetBtn, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(state->targetBtn, true); lv_obj_add_event_cb(state->targetBtn, onTargetClicked, LV_EVENT_CLICKED, ctx); lv_obj_t* tapLabel = lv_label_create(state->targetBtn); @@ -221,6 +235,7 @@ void reactionGameCreateWidgets(lv_obj_t* parent, Context* ctx) { srand(static_cast(tt::kernel::getMillis())); state->round = 0; state->score = 0; + state->fastHits = 0; state->phase = ReactionGameState::Phase::WaitForTarget; startRound(ctx); } diff --git a/Apps/TamaTac/main/Source/ReactionGame.h b/Apps/TamaTac/main/Source/ReactionGame.h index 1ae1c11..15550a5 100644 --- a/Apps/TamaTac/main/Source/ReactionGame.h +++ b/Apps/TamaTac/main/Source/ReactionGame.h @@ -21,7 +21,8 @@ struct ReactionGameState { // Game state int round = 0; - int score = 0; // 0-3 based on reaction quality + int score = 0; // 0-3 rounds tapped in time (any speed) + int fastHits = 0; // 0-3 rounds tapped within GOOD_TIME_MS uint32_t targetShowTime = 0; // When target appeared (millis) }; diff --git a/Apps/TamaTac/main/Source/SettingsView.cpp b/Apps/TamaTac/main/Source/SettingsView.cpp index 1feac18..63304b0 100644 --- a/Apps/TamaTac/main/Source/SettingsView.cpp +++ b/Apps/TamaTac/main/Source/SettingsView.cpp @@ -4,10 +4,12 @@ */ #include "SettingsView.h" +#include "Layout.h" #include "TamaTac.h" #include #include #include +#include namespace { @@ -45,47 +47,54 @@ lv_obj_t* createSettingRow(lv_obj_t* parentContainer, const char* labelText, boo void onSoundToggled(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); SettingsViewState* state = &ctx->settingsView; - if (state->soundSwitch == nullptr || state->decayDropdown == nullptr) return; + if (state->soundSwitch == nullptr) return; bool isChecked = lv_obj_has_state(state->soundSwitch, LV_STATE_CHECKED); tamaTacSetSoundEnabled(ctx, isChecked); - // Read decay speed from UI widget instead of redundant preferences load - DecaySpeed decaySpeed = static_cast(lv_dropdown_get_selected(state->decayDropdown)); - settingsViewSaveSettings(isChecked, decaySpeed); + // Decay speed lives in Context (single source of truth) + settingsViewSaveSettings(isChecked, ctx->decaySpeed); } -void onDecayChanged(lv_event_t* e) { +void syncDecayChips(SettingsViewState* state, DecaySpeed selected) { + for (int i = 0; i < 3; i++) { + if (state->decayChips[i] != nullptr) { + lv_obj_set_state(state->decayChips[i], LV_STATE_CHECKED, i == static_cast(selected)); + } + } +} + +void onDecayChipPressed(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); SettingsViewState* state = &ctx->settingsView; - if (state->decayDropdown == nullptr || state->soundSwitch == nullptr) return; - - uint16_t selected = lv_dropdown_get_selected(state->decayDropdown); - - if (selected > 2) { - selected = 1; + auto* chip = static_cast(lv_event_get_current_target_obj(e)); + + for (int i = 0; i < 3; i++) { + if (state->decayChips[i] == chip) { + DecaySpeed newSpeed = static_cast(i); + tamaTacSetDecaySpeed(ctx, newSpeed); + syncDecayChips(state, newSpeed); + + bool soundEnabled = state->soundSwitch != nullptr && + lv_obj_has_state(state->soundSwitch, LV_STATE_CHECKED); + settingsViewSaveSettings(soundEnabled, newSpeed); + break; + } } - DecaySpeed newSpeed = static_cast(selected); - - tamaTacSetDecaySpeed(ctx, newSpeed); - - // Read sound state from UI widget instead of redundant preferences load - bool soundEnabled = lv_obj_has_state(state->soundSwitch, LV_STATE_CHECKED); - settingsViewSaveSettings(soundEnabled, newSpeed); } } // namespace +constexpr const char* DECAY_NAMES[] = { "Slow", "Normal", "Fast" }; + void settingsViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { SettingsViewState* state = &ctx->settingsView; - // Detect screen size for responsive layout - // Use display resolution for reliable sizing (parent may not be laid out yet on first load) - lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr); - lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr); - bool isSmall = (screenWidth < 280 || screenHeight < 180); - bool isXLarge = (screenWidth >= 600); + // Shared viewport detection (display resolution + orientation) + TamaViewport vp = tamaGetViewport(); + bool isSmall = tamaIsSmall(vp); + bool isXLarge = tamaIsXLarge(vp); // Load current settings bool soundEnabled = true; @@ -105,7 +114,9 @@ void settingsViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_style_border_width(state->mainWrapper, 0, 0); lv_obj_set_flex_flow(state->mainWrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(state->mainWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_remove_flag(state->mainWrapper, LV_OBJ_FLAG_SCROLLABLE); + // Vertical scroll safety: rows + chips can overflow short screens + lv_obj_set_scrollable(state->mainWrapper, true); + lv_obj_set_scroll_dir(state->mainWrapper, LV_DIR_VER); // Sound setting row lv_obj_t* soundRow = createSettingRow(state->mainWrapper, "Sound", isSmall, isXLarge); @@ -118,23 +129,38 @@ void settingsViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { } lv_obj_add_event_cb(state->soundSwitch, onSoundToggled, LV_EVENT_VALUE_CHANGED, ctx); - // Decay speed row + // Decay speed row (label + 3 exclusive chips, wraps on narrow portrait screens) lv_obj_t* decayRow = createSettingRow(state->mainWrapper, "Speed", isSmall, isXLarge); - - state->decayDropdown = lv_dropdown_create(decayRow); - lv_dropdown_set_options(state->decayDropdown, "Slow\nNormal\nFast"); - lv_dropdown_set_selected(state->decayDropdown, static_cast(decaySpeed)); - lv_obj_set_width(state->decayDropdown, isSmall ? 80 : (isXLarge ? 140 : 100)); - lv_obj_set_style_bg_color(state->decayDropdown, lv_color_hex(0x1a1a2e), LV_PART_MAIN); - lv_obj_set_style_text_color(state->decayDropdown, lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_add_event_cb(state->decayDropdown, onDecayChanged, LV_EVENT_VALUE_CHANGED, ctx); + lv_obj_set_flex_flow(decayRow, LV_FLEX_FLOW_ROW_WRAP); + + lv_obj_t* chips = lv_obj_create(decayRow); + lv_obj_set_size(chips, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(chips, LV_FLEX_FLOW_ROW_WRAP); + lv_obj_set_style_bg_opa(chips, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(chips, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(chips, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(chips, isSmall ? 2 : 4, LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(chips, isSmall ? 2 : 4, LV_STATE_DEFAULT); + lv_obj_set_scrollable(chips, false); + + for (int32_t i = 0; i < 3; i++) { + auto* chip = lvgl_chip_create(chips); + lv_obj_t* chipLabel = lv_label_create(chip); + lv_label_set_text(chipLabel, DECAY_NAMES[i]); + lv_obj_center(chipLabel); + lv_obj_add_event_cb(chip, onDecayChipPressed, LV_EVENT_SHORT_CLICKED, ctx); + state->decayChips[i] = chip; + } + syncDecayChips(state, decaySpeed); } void settingsViewStop(Context* ctx) { SettingsViewState* state = &ctx->settingsView; state->mainWrapper = nullptr; state->soundSwitch = nullptr; - state->decayDropdown = nullptr; + for (int i = 0; i < 3; i++) { + state->decayChips[i] = nullptr; + } } void settingsViewLoadSettings(bool* soundEnabled, DecaySpeed* decaySpeed) { diff --git a/Apps/TamaTac/main/Source/SettingsView.h b/Apps/TamaTac/main/Source/SettingsView.h index c321e0a..31cb305 100644 --- a/Apps/TamaTac/main/Source/SettingsView.h +++ b/Apps/TamaTac/main/Source/SettingsView.h @@ -18,7 +18,7 @@ enum class DecaySpeed { struct SettingsViewState { lv_obj_t* mainWrapper = nullptr; lv_obj_t* soundSwitch = nullptr; - lv_obj_t* decayDropdown = nullptr; + lv_obj_t* decayChips[3] = {nullptr, nullptr, nullptr}; }; void settingsViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx); diff --git a/Apps/TamaTac/main/Source/StatsView.cpp b/Apps/TamaTac/main/Source/StatsView.cpp index 44b9bbb..3613651 100644 --- a/Apps/TamaTac/main/Source/StatsView.cpp +++ b/Apps/TamaTac/main/Source/StatsView.cpp @@ -4,6 +4,7 @@ */ #include "StatsView.h" +#include "Layout.h" #include "TamaTac.h" #include @@ -36,12 +37,10 @@ lv_obj_t* createStatRow(lv_obj_t* parentContainer, const char* labelText, lv_col void statsViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { StatsViewState* state = &ctx->statsView; - // Detect screen size for responsive layout - // Use display resolution for reliable sizing (parent may not be laid out yet on first load) - lv_coord_t screenWidth = lv_display_get_horizontal_resolution(nullptr); - lv_coord_t screenHeight = lv_display_get_vertical_resolution(nullptr); - bool isSmall = (screenWidth < 280 || screenHeight < 180); - bool isXLarge = (screenWidth >= 600); + // Shared viewport detection (display resolution + orientation) + TamaViewport vp = tamaGetViewport(); + bool isSmall = tamaIsSmall(vp); + bool isXLarge = tamaIsXLarge(vp); // Scaled dimensions int padAll = isSmall ? 6 : (isXLarge ? 24 : 12); @@ -56,7 +55,9 @@ void statsViewCreateWidgets(lv_obj_t* parentWidget, Context* ctx) { lv_obj_set_style_border_width(state->mainWrapper, 0, 0); lv_obj_set_flex_flow(state->mainWrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(state->mainWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); - lv_obj_remove_flag(state->mainWrapper, LV_OBJ_FLAG_SCROLLABLE); + // Vertical scroll safety: 7 rows can overflow short landscape screens + lv_obj_set_scrollable(state->mainWrapper, true); + lv_obj_set_scroll_dir(state->mainWrapper, LV_DIR_VER); // Title row (stage/age on left, status on right) lv_obj_t* titleRow = lv_obj_create(state->mainWrapper); diff --git a/Apps/TamaTac/main/Source/TamaTac.cpp b/Apps/TamaTac/main/Source/TamaTac.cpp index d80fcb1..c619b20 100644 --- a/Apps/TamaTac/main/Source/TamaTac.cpp +++ b/Apps/TamaTac/main/Source/TamaTac.cpp @@ -113,14 +113,17 @@ void onResetClicked([[maybe_unused]] lv_event_t* e) { "Reset", "Cancel", }; - app_start_for_result("AlertDialog", 4, argv, ctx->appInstanceId, &ctx->resetDialogId); + app_start_for_result("tactility.alertdialog", 4, argv, ctx->appInstanceId, &ctx->resetDialogId); } void onTimerUpdate(TimerHandle_t timer) { auto* ctx = static_cast(pvTimerGetTimerID(timer)); - // Hold mutex for entire callback so tamaTacTeardown() can block until we finish - if (ctx->timerMutex == nullptr || xSemaphoreTake(ctx->timerMutex, 0) != pdTRUE) return; + // Hold mutex for entire callback so tamaTacTeardown() can block until we finish. + // Brief wait instead of a zero-timeout poll: an action handler holding the + // mutex would otherwise make us skip this whole 30s tick (no save, no + // achievements, no death recording). 200ms is nothing against the period. + if (ctx->timerMutex == nullptr || xSemaphoreTake(ctx->timerMutex, pdMS_TO_TICKS(200)) != pdTRUE) return; bool wasAlive = !ctx->petLogic.isDead(); // Capture stage before update() - checkHealth() sets stage to Ghost on death @@ -201,7 +204,7 @@ void tamaTacInit(Context* ctx) { void tamaTacCreateWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(parent, 0, 0); lv_obj_set_style_pad_row(parent, 0, 0); @@ -218,7 +221,7 @@ void tamaTacCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_all(ctx->wrapperWidget, 0, 0); lv_obj_set_style_border_width(ctx->wrapperWidget, 0, 0); lv_obj_set_style_bg_opa(ctx->wrapperWidget, LV_OPA_TRANSP, 0); - lv_obj_remove_flag(ctx->wrapperWidget, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->wrapperWidget, false); // Rebuild whichever view was active. On first creation this is None (-> Main). On a // resurface after this window was buried (e.g. by the reset AlertDialog, or by an unrelated @@ -387,13 +390,17 @@ void tamaTacHandlePlayAction(Context* ctx) { bool canPlay = !ctx->petLogic.isDead(); DayPhase phase = canPlay ? ctx->petLogic.getDayPhase() : DayPhase::Day; - if (ctx->timerMutex) xSemaphoreGive(ctx->timerMutex); - + // Achievement storage is serialized by timerMutex: unlock while held if (canPlay) { achievementsUnlock(AchievementId::FirstPlay); if (phase == DayPhase::Night) { achievementsUnlock(AchievementId::NightOwl); } + } + + if (ctx->timerMutex) xSemaphoreGive(ctx->timerMutex); + + if (canPlay) { if (rand() % 2 == 0) { showPatternGame(ctx); } else { @@ -436,9 +443,10 @@ void tamaTacOnReactionGameComplete(Context* ctx, int score, bool won) { ctx->petLogic.applyPlayResult(clampedScore, REACTION_GAME_MAX_ROUNDS); ctx->petLogic.saveState(); + if (won) achievementsUnlock(AchievementId::PerfectGame); + if (ctx->timerMutex) xSemaphoreGive(ctx->timerMutex); - if (won) achievementsUnlock(AchievementId::PerfectGame); if (ctx->sfxEngine) ctx->sfxEngine->play(SfxId::Play); showMainView(ctx); @@ -451,9 +459,10 @@ void tamaTacOnPatternGameComplete(Context* ctx, int score, bool won) { ctx->petLogic.applyPlayResult(clampedScore, PATTERN_GAME_MAX_ROUNDS); ctx->petLogic.saveState(); + if (won) achievementsUnlock(AchievementId::PerfectGame); + if (ctx->timerMutex) xSemaphoreGive(ctx->timerMutex); - if (won) achievementsUnlock(AchievementId::PerfectGame); if (ctx->sfxEngine) ctx->sfxEngine->play(SfxId::Play); showMainView(ctx); diff --git a/Apps/TamaTac/main/Source/main.cpp b/Apps/TamaTac/main/Source/main.cpp index bb398ec..ff54657 100644 --- a/Apps/TamaTac/main/Source/main.cpp +++ b/Apps/TamaTac/main/Source/main.cpp @@ -57,6 +57,9 @@ int main(int argc, char* argv[]) { ctx->petLogic.reset(); ctx->petLogic.saveState(); ctx->lastKnownStage = LifeStage::Egg; + ctx->lastPetTime = 0; // Clear tap cooldown for the new pet + // Achievements + cemetery intentionally survive: they + // record the player's history, not this pet's life. ctx->pendingResetUI = true; // Defer UI update to MainView's anim timer if (ctx->timerMutex) xSemaphoreGive(ctx->timerMutex); diff --git a/Apps/TamaTac/manifest.properties b/Apps/TamaTac/manifest.properties index 8268fc5..2c88015 100644 --- a/Apps/TamaTac/manifest.properties +++ b/Apps/TamaTac/manifest.properties @@ -2,10 +2,11 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.tamatac -version.name=0.16.0 -version.code=16 +version.name=0.17.0 +version.code=17 +requires.ram=2M name=TamaTac -description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM. +description=Virtual pet inspired by Tamagotchi. app.0.id=tactility.tamatac app.0.name=TamaTac app.0.binary=tamatac diff --git a/Apps/TodoList/main/Source/TodoList.cpp b/Apps/TodoList/main/Source/TodoList.cpp index d9de501..bce3de5 100644 --- a/Apps/TodoList/main/Source/TodoList.cpp +++ b/Apps/TodoList/main/Source/TodoList.cpp @@ -278,7 +278,7 @@ void todoListCreateWidgets(lv_obj_t* parent, void* userData) { loadTodos(ctx); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); /* Toolbar */ @@ -295,7 +295,7 @@ void todoListCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_column(countWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(countWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(countWrapper, 0, LV_STATE_DEFAULT); - lv_obj_remove_flag(countWrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(countWrapper, false); ctx->countLabel = lv_label_create(countWrapper); lv_obj_set_size(ctx->countLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); @@ -304,27 +304,8 @@ void todoListCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_text_font(ctx->countLabel, lv_font_get_default(), 0); lv_obj_set_style_text_color(ctx->countLabel, lv_palette_main(LV_PALETTE_CYAN), LV_PART_MAIN); - auto ui_density = lvgl_get_ui_density(); - auto toolbar_height = getToolbarHeight(ui_density); - auto icon_padding = getActionIconPadding(ui_density); - - lv_obj_t* btnWrapper = lv_obj_create(toolbar); - lv_obj_set_width(btnWrapper, LV_SIZE_CONTENT); - lv_obj_set_flex_flow(btnWrapper, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_all(btnWrapper, icon_padding / 2, LV_STATE_DEFAULT); - lv_obj_set_style_pad_column(btnWrapper, 4, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(btnWrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(btnWrapper, 0, LV_STATE_DEFAULT); - - lv_obj_t* clearBtn = lv_button_create(btnWrapper); - lv_obj_set_size(clearBtn, toolbar_height - icon_padding, toolbar_height - icon_padding); - lv_obj_set_style_pad_all(clearBtn, 0, LV_STATE_DEFAULT); - lv_obj_align(clearBtn, LV_ALIGN_CENTER, 0, 0); - lv_obj_add_event_cb(clearBtn, onClearDoneClicked, LV_EVENT_CLICKED, ctx); - - lv_obj_t* clearIcon = lv_label_create(clearBtn); - lv_label_set_text(clearIcon, LV_SYMBOL_TRASH); - lv_obj_align(clearIcon, LV_ALIGN_CENTER, 0, 0); + // Create clear button + lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearDoneClicked, ctx); /* Container */ lv_obj_t* cont = lv_obj_create(parent); @@ -333,7 +314,7 @@ void todoListCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(cont, 4, 0); lv_obj_set_style_pad_gap(cont, 4, 0); - lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(cont, false); lv_obj_set_style_border_width(cont, 0, 0); /* Scrollable list */ @@ -348,7 +329,7 @@ void todoListCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_align(ctx->inputRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(ctx->inputRow, 0, 0); lv_obj_set_style_pad_gap(ctx->inputRow, 4, 0); - lv_obj_remove_flag(ctx->inputRow, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->inputRow, false); lv_obj_set_style_border_width(ctx->inputRow, 0, 0); ctx->inputTa = lv_textarea_create(ctx->inputRow); diff --git a/Apps/TodoList/manifest.properties b/Apps/TodoList/manifest.properties index fec17d3..da38784 100644 --- a/Apps/TodoList/manifest.properties +++ b/Apps/TodoList/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.todolist -version.name=0.19.0 -version.code=19 +version.name=0.20.0 +version.code=20 name=Todo List description=Simple task list manager app.0.id=tactility.todolist diff --git a/Apps/Tone/main/Source/ToneUi.cpp b/Apps/Tone/main/Source/ToneUi.cpp index 0c2c271..cd5f6d0 100644 --- a/Apps/Tone/main/Source/ToneUi.cpp +++ b/Apps/Tone/main/Source/ToneUi.cpp @@ -170,9 +170,9 @@ void updateFrequencyInput(Context* ctx) { void updateSweepCard(Context* ctx) { if (ctx->sweepCard != nullptr) { if (ctx->playback.sweep.load()) { - lv_obj_remove_flag(ctx->sweepCard, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->sweepCard, false); } else { - lv_obj_add_flag(ctx->sweepCard, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_hidden(ctx->sweepCard, true); } } } @@ -423,7 +423,7 @@ lv_obj_t* createCard(lv_obj_t* parent) { auto* card = lvgl_card_create(parent); lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); - lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(card, false); return card; } @@ -434,7 +434,7 @@ lv_obj_t* createRow(lv_obj_t* parent, lv_coord_t gap) { lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW_WRAP); lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_gap(row, gap, LV_STATE_DEFAULT); - lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(row, false); return row; } @@ -458,7 +458,7 @@ void toneCreateWidgetsImpl(lv_obj_t* parent, Context* ctx) { const lv_coord_t fieldHeight = static_cast(fontHeight * 1.8f); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); auto* toolbar = lvgl_toolbar_create(parent, "Tone"); lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); diff --git a/Apps/Tone/manifest.properties b/Apps/Tone/manifest.properties index 3e3ed69..21a298a 100644 --- a/Apps/Tone/manifest.properties +++ b/Apps/Tone/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=all id=tactility.tone -version.name=0.2.0 -version.code=2 +version.name=0.3.0 +version.code=3 name=Tone description=Tone generator and frequency sweeper app.0.id=tactility.tone diff --git a/Apps/TwoEleven/main/Source/TwoEleven.cpp b/Apps/TwoEleven/main/Source/TwoEleven.cpp index 11b3e72..f8ca7ac 100644 --- a/Apps/TwoEleven/main/Source/TwoEleven.cpp +++ b/Apps/TwoEleven/main/Source/TwoEleven.cpp @@ -31,19 +31,6 @@ bool getPreferencesPath(std::string& outPath) { return true; } -uint32_t getToolbarHeight(UiDensity uiDensity) { - if (uiDensity == LVGL_UI_DENSITY_COMPACT) { - return lvgl_get_text_font_height(FONT_SIZE_DEFAULT) * 1.4f; - } else { - return lvgl_get_text_font_height(FONT_SIZE_LARGE) * 2.2f; - } -} - -uint32_t getActionIconPadding(UiDensity uiDensity) { - auto toolbar_height = getToolbarHeight(uiDensity); - return (uiDensity != LVGL_UI_DENSITY_COMPACT) ? (uint32_t)(toolbar_height * 0.2f) : 8; -} - void loadHighScores(Context* ctx) { std::string path; if (!getPreferencesPath(path)) return; @@ -156,7 +143,7 @@ void newGameBtnEvent(lv_event_t* e) { } void createGame(Context* ctx, lv_obj_t* parent, uint16_t size, lv_obj_t* tb) { - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); // Create game widget @@ -176,7 +163,7 @@ void createGame(Context* ctx, lv_obj_t* parent, uint16_t size, lv_obj_t* tb) { lv_obj_set_style_pad_column(ctx->scoreWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(ctx->scoreWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ctx->scoreWrapper, 0, LV_STATE_DEFAULT); - lv_obj_remove_flag(ctx->scoreWrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->scoreWrapper, false); // Create score label ctx->scoreLabel = lv_label_create(ctx->scoreWrapper); @@ -188,28 +175,8 @@ void createGame(Context* ctx, lv_obj_t* parent, uint16_t size, lv_obj_t* tb) { lv_obj_set_style_text_color(ctx->scoreLabel, lv_palette_main(LV_PALETTE_AMBER), LV_PART_MAIN); lv_obj_add_event_cb(ctx->gameObject, twoElevenEventCb, LV_EVENT_VALUE_CHANGED, ctx); - auto ui_density = lvgl_get_ui_density(); - auto toolbar_height = getToolbarHeight(ui_density); - auto icon_padding = getActionIconPadding(ui_density); - - // Create new game button wrapper - ctx->newGameWrapper = lv_obj_create(tb); - lv_obj_set_width(ctx->newGameWrapper, LV_SIZE_CONTENT); - lv_obj_set_flex_flow(ctx->newGameWrapper, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_all(ctx->newGameWrapper, icon_padding / 2, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ctx->newGameWrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ctx->newGameWrapper, 0, LV_STATE_DEFAULT); - // Create new game button - lv_obj_t* newGameBtn = lv_btn_create(ctx->newGameWrapper); - lv_obj_set_size(newGameBtn, toolbar_height - icon_padding, toolbar_height - icon_padding); - lv_obj_set_style_pad_all(newGameBtn, 0, LV_STATE_DEFAULT); - lv_obj_align(newGameBtn, LV_ALIGN_CENTER, 0, 0); - lv_obj_add_event_cb(newGameBtn, newGameBtnEvent, LV_EVENT_CLICKED, ctx); - - lv_obj_t* btnIcon = lv_image_create(newGameBtn); - lv_image_set_src(btnIcon, LV_SYMBOL_REFRESH); - lv_obj_align(btnIcon, LV_ALIGN_CENTER, 0, 0); + lvgl_toolbar_add_text_button_action(tb, LV_SYMBOL_REFRESH, newGameBtnEvent, ctx); } } // namespace @@ -218,7 +185,7 @@ void twoElevenCreateWidgets(lv_obj_t* parent, void* userData) { ESP_LOGI("TwoEleven", "twoElevenCreateWidgets called, parent=%p", parent); auto* ctx = static_cast(userData); - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(parent, false); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); // Create toolbar @@ -233,7 +200,7 @@ void twoElevenCreateWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_pad_row(ctx->mainWrapper, 2, LV_PART_MAIN); lv_obj_set_style_pad_column(ctx->mainWrapper, 2, LV_PART_MAIN); lv_obj_set_style_border_width(ctx->mainWrapper, 0, LV_PART_MAIN); - lv_obj_remove_flag(ctx->mainWrapper, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollable(ctx->mainWrapper, false); // Load high scores on first build if (!ctx->highScoresLoaded) { diff --git a/Apps/TwoEleven/main/Source/TwoElevenUi.c b/Apps/TwoEleven/main/Source/TwoElevenUi.c index ff76ed0..a6418f4 100644 --- a/Apps/TwoEleven/main/Source/TwoElevenUi.c +++ b/Apps/TwoEleven/main/Source/TwoElevenUi.c @@ -130,8 +130,8 @@ lv_obj_t * twoeleven_create(lv_obj_t * parent, uint16_t matrix_size) lv_obj_set_style_border_width(game_2048->btnm, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(game_2048->btnm, ELEVENTWO_BG_COLOR, LV_PART_MAIN); lv_group_remove_obj(game_2048->btnm); - lv_obj_add_flag(game_2048->btnm, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS); - lv_obj_remove_flag(game_2048->btnm, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_set_send_draw_task_events(game_2048->btnm, true); + lv_obj_set_gesture_bubble(game_2048->btnm, false); lv_btnmatrix_set_map(game_2048->btnm, (const char **)game_2048->btnm_map); lv_btnmatrix_set_btn_ctrl_all(game_2048->btnm, LV_BTNMATRIX_CTRL_DISABLED); diff --git a/Apps/TwoEleven/manifest.properties b/Apps/TwoEleven/manifest.properties index 9f2be8a..3b88a8b 100644 --- a/Apps/TwoEleven/manifest.properties +++ b/Apps/TwoEleven/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.twoeleven -version.name=0.20.0 -version.code=20 +version.name=0.21.0 +version.code=21 name=2048 description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert). app.0.id=tactility.twoeleven diff --git a/Apps/UnitLcdGfxDemo/CMakeLists.txt b/Apps/UnitLcdGfxDemo/CMakeLists.txt new file mode 100644 index 0000000..b2c1d3b --- /dev/null +++ b/Apps/UnitLcdGfxDemo/CMakeLists.txt @@ -0,0 +1,13 @@ +# tactility-cmakelists-version: 1 +cmake_minimum_required(VERSION 3.20) + +if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) + message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") +endif() + +get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +tactility_project_pre(unitlcdgfxdemo) +project(unitlcdgfxdemo) +tactility_project_post(unitlcdgfxdemo) diff --git a/Apps/UnitLcdGfxDemo/README.md b/Apps/UnitLcdGfxDemo/README.md new file mode 100644 index 0000000..349b6c8 --- /dev/null +++ b/Apps/UnitLcdGfxDemo/README.md @@ -0,0 +1,60 @@ +# UnitLCD Gfx Demo + +A hardware test for the `SinkTarget` + `UnitLcdSink` path. It drives an M5Stack LCD Unit over +I2C while the primary display shows a status screen through the normal `gfx::Target` path. + +## What it does + +- Finds the first Grove I2C port. +- Tries a PaHub (channels 0-5), then a direct connection, and initialises the LCD Unit. +- Switches the unit to landscape (240×135), draws the static chrome (title, border, footer) + directly with the unit's own primitives, and creates a sink for the animated play area only: + +```cpp +UnitLcdSink sink(lcd, LCD_PLAY_LEFT, LCD_PLAY_TOP); +gfx::SinkTarget target(sink, LCD_PLAY_WIDTH, LCD_PLAY_HEIGHT, DISPLAY_COLOR_FORMAT_RGB565); +``` + +- Draws a bouncing ball in a 228×72 play area. The partial frame needs about 32 KB instead of the + 64.8 KB of a full 240×135 frame, which matters on boards without PSRAM. If even that does not fit + in one contiguous block, the play area shrinks to 228×56, 228×40 or 228×24 and the border/footer + follow it. +- Only the ball and the frame counter are redrawn after the first frame, so the I2C traffic is + dirty regions rather than the whole frame. +- On portrait-native primary panels (e.g. Tab5), uses `gfx::Target::set_transform()` to present a + landscape logical frame via the PPA, and scales the status text to the logical size. +- The primary display shows whether the LCD was found, its size, and whether it is behind a PaHub. +- On no-PSRAM boards (e.g. Cardputer) the LCD frame is allocated first; the primary status target is + optional and is created only if the remaining RAM allows it. The LCD still runs without it. +- The colour of the chrome and the size of the play area are compile-time constants, so the same + demo scales down to even smaller packs by shrinking `LCD_PLAY_HEIGHT`. + +Any input exits. The LCD is cleared on startup and shutdown because its framebuffer persists across +runs, then the backlight is turned off. + +## Wiring + +Connect the LCD Unit to a Grove I2C port, directly or through a PaHub. The unit must be at the +standard address (`0x3E`). The app handles PaHub channel selection for each frame. + +## Build + +```sh +tt app build --path Apps/UnitLcdGfxDemo --architecture esp32s3 +tt app bir --path Apps/UnitLcdGfxDemo --architecture esp32s3 --host +``` + +The app compiles `UnitLcd.cpp`, `UnitPaHub.cpp`, `Libraries/Gfx/Source`, `Libraries/Input/Source` +and the header-only `Libraries/UnitLcdGfx` adapter. It does not modify `M5UnitModules`. + +## Notes + +- The unit is slow compared to the primary panel; the 40 ms frame budget is a compromise between a + visible animation and I2C bandwidth. +- `UnitLcdSink` accepts an origin, so a sink can render a sub-frame instead of the whole panel. The + demo uses it to keep the frame buffer near 32 KB; a full frame (64.8 KB) may not fit as one + contiguous block on no-PSRAM boards such as the Cardputer. +- The LCD is cleared on startup and shutdown because its framebuffer survives app restarts. +- If the LCD is not found, the primary screen shows an error and waits for input. +- This is also a useful reference for adding sinks for other I2C/SPI panels or OLEDs: implement + `gfx::Sink::begin/present/end`, then hand it to `gfx::SinkTarget`. diff --git a/Apps/UnitLcdGfxDemo/main/CMakeLists.txt b/Apps/UnitLcdGfxDemo/main/CMakeLists.txt new file mode 100644 index 0000000..efa4ce1 --- /dev/null +++ b/Apps/UnitLcdGfxDemo/main/CMakeLists.txt @@ -0,0 +1,20 @@ +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") + +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) +file(GLOB_RECURSE GFX_FILES ../../../Libraries/Gfx/Source/*.c*) +file(GLOB_RECURSE INPUT_FILES ../../../Libraries/Input/Source/*.c*) +file(GLOB UNIT_FILES + ../../../Libraries/M5UnitModules/Source/UnitLcd.cpp + ../../../Libraries/M5UnitModules/Source/UnitPaHub.cpp +) + +tactility_component_register( + SRCS ${SOURCE_FILES} ${GFX_FILES} ${INPUT_FILES} ${UNIT_FILES} + INCLUDE_DIRS + "Include" + ../../../Libraries/Gfx/Include + ../../../Libraries/Input/Include + ../../../Libraries/M5UnitModules/Include + ../../../Libraries/UnitLcdGfx/Include + REQUIRES graphics-module esp_driver_ppa +) diff --git a/Apps/UnitLcdGfxDemo/main/Include/Application.h b/Apps/UnitLcdGfxDemo/main/Include/Application.h new file mode 100644 index 0000000..bf3c988 --- /dev/null +++ b/Apps/UnitLcdGfxDemo/main/Include/Application.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +struct Device; + +void runApplication(struct Device* display, AnyInput* input); diff --git a/Apps/UnitLcdGfxDemo/main/Include/GroveLookup.h b/Apps/UnitLcdGfxDemo/main/Include/GroveLookup.h new file mode 100644 index 0000000..431c2e5 --- /dev/null +++ b/Apps/UnitLcdGfxDemo/main/Include/GroveLookup.h @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +/** Finds the I2C controller device exposed by the first grove port currently in I2C mode. */ +Device* findGroveI2cDevice(); diff --git a/Apps/UnitLcdGfxDemo/main/Source/Application.cpp b/Apps/UnitLcdGfxDemo/main/Source/Application.cpp new file mode 100644 index 0000000..c6d0789 --- /dev/null +++ b/Apps/UnitLcdGfxDemo/main/Source/Application.cpp @@ -0,0 +1,454 @@ +#include "Application.h" +#include "GroveLookup.h" + +#include +#include +#include + +#include + +#ifdef ESP_PLATFORM +#include +#endif +#include +#include + +#include + +#include +#include +#include +#include +#include + +constexpr auto TAG = "UnitLcdGfxDemo"; + +// Tab5's panel is native portrait; direct rendering does not rotate it for us. Turn it into a +// landscape logical frame with the PPA. If the status appears upside down, use 270 instead. +constexpr int PRIMARY_LANDSCAPE_ROTATION = 90; + +namespace { + +constexpr uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) { + return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); +} + +// The M5 LCD Unit is 240x135 in landscape. Its frame buffer lives in internal RAM, and on +// no-PSRAM boards (e.g. Cardputer) a full 240x135 frame (64.8 KB) may not be available as a single +// contiguous block. The demo therefore only drives the animated play area through the sink and +// draws the surrounding chrome directly on the unit with its own hardware primitives. The +// preferred play area is 228x72 (about 32 KB); smaller heights are tried when memory is tighter. +constexpr int LCD_PLAY_LEFT = 6; +constexpr int LCD_PLAY_TOP = 40; +constexpr int LCD_PLAY_WIDTH = 228; +constexpr int LCD_PLAY_HEIGHT = 72; +constexpr int LCD_PLAY_HEIGHT_FALLBACKS[] = { 56, 40, 24 }; +constexpr int LCD_BALL_RADIUS = 7; + +constexpr uint16_t LCD_BACKGROUND = rgb565(6, 10, 20); +constexpr uint16_t LCD_CHROME = rgb565(40, 60, 90); +constexpr uint16_t LCD_TITLE_COLOR = 0xF81F; +constexpr uint16_t LCD_SUBTITLE_COLOR = 0x07FF; +constexpr uint16_t LCD_BALL_COLOR = 0xF800; + +int clampInt(int value, int low, int high) { + return value < low ? low : (value > high ? high : value); +} + +int titleScaleFor(const gfx::Surface& surface) { + return clampInt(std::min(surface.width(), surface.height()) / 140, 1, 5); +} + +int bodyScaleFor(const gfx::Surface& surface) { + return clampInt(std::min(surface.width(), surface.height()) / 200, 1, 3); +} + +void preparePrimary(gfx::Target& primary) { + const gfx::Info& info = primary.info(); + if (info.width < info.height) { + const gfx::Transform transform { + info.height, + info.width, + PRIMARY_LANDSCAPE_ROTATION, + }; + if (!primary.set_transform(transform)) { + ESP_LOGW(TAG, "Primary rotation unavailable; keeping native orientation"); + } + } +} + +bool takeExit(struct AppEventSubscription& subscription, AnyInput& input) { + bool shouldExit = false; + struct AppEvent event {}; + while (app_event_poll(&subscription, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldExit = true; + } + } + return shouldExit || input.activity(); +} + +void waitForExit(struct AppEventSubscription& subscription, AnyInput& input) { + while (!takeExit(subscription, input)) { + tt::kernel::delayTicks(tt::kernel::millisToTicks(50)); + } +} + +void showPrimaryMessage(gfx::Target& primary, const char* title, const char* line1, const char* line2) { + primary.render([&](gfx::Surface& surface) { + surface.clear(0x0000); + + const int titleScale = titleScaleFor(surface); + const int bodyScale = bodyScaleFor(surface); + const int titleHeight = gfx::lineHeight(gfx::FONT_8X16, static_cast(titleScale)); + const int bodyHeight = gfx::lineHeight(gfx::FONT_5X7, static_cast(bodyScale)); + const int gap = bodyHeight; + const int blockHeight = titleHeight + gap + bodyHeight * 2; + const int startY = std::max(4, (surface.height() - blockHeight) / 2); + + gfx::TextStyle titleStyle; + titleStyle.font = &gfx::FONT_8X16; + titleStyle.foreground = 0xFFFF; + titleStyle.align = gfx::TextAlign::Center; + titleStyle.scale = static_cast(titleScale); + surface.drawText(surface.width() / 2, startY, title, titleStyle); + + gfx::TextStyle body; + body.font = &gfx::FONT_5X7; + body.foreground = 0x07FF; + body.align = gfx::TextAlign::Center; + body.scale = static_cast(bodyScale); + surface.drawText(surface.width() / 2, startY + titleHeight + gap, line1, body); + surface.drawText(surface.width() / 2, startY + titleHeight + gap + bodyHeight, line2, body); + + gfx::TextStyle hint; + hint.font = &gfx::FONT_5X7; + hint.foreground = 0x8410; + hint.align = gfx::TextAlign::Center; + hint.verticalAlign = gfx::TextVerticalAlign::Bottom; + hint.scale = static_cast(bodyScale); + surface.drawText(surface.width() / 2, surface.height() - 6, "Any input to exit", hint); + }, gfx::Prepare::Overwrite); + + if (primary.info().slowRefresh) { + primary.refresh(); + } +} + +void drawPrimaryStatus(gfx::Target& primary, int lcdWidth, int lcdHeight, bool viaHub, int channel) { + primary.render([&](gfx::Surface& surface) { + surface.clear(0x0000); + + const int titleScale = titleScaleFor(surface); + const int bodyScale = bodyScaleFor(surface); + const int titleHeight = gfx::lineHeight(gfx::FONT_8X16, static_cast(titleScale)); + const int bodyHeight = gfx::lineHeight(gfx::FONT_5X7, static_cast(bodyScale)); + const int gap = bodyHeight; + const int blockHeight = titleHeight + gap + bodyHeight * 3; + const int startY = std::max(4, (surface.height() - blockHeight) / 2); + + gfx::TextStyle title; + title.font = &gfx::FONT_8X16; + title.foreground = 0xF81F; + title.align = gfx::TextAlign::Center; + title.scale = static_cast(titleScale); + surface.drawText(surface.width() / 2, startY, "UnitLCD Gfx Sink", title); + + char line[48]; + gfx::TextStyle body; + body.font = &gfx::FONT_5X7; + body.foreground = 0xFFFF; + body.align = gfx::TextAlign::Center; + body.scale = static_cast(bodyScale); + + int y = startY + titleHeight + gap; + std::snprintf(line, sizeof(line), "LCD %dx%d", lcdWidth, lcdHeight); + surface.drawText(surface.width() / 2, y, line, body); + + y += bodyHeight; + if (viaHub) { + std::snprintf(line, sizeof(line), "via PaHub channel %d", channel); + } else { + std::snprintf(line, sizeof(line), "direct I2C"); + } + body.foreground = 0x07FF; + surface.drawText(surface.width() / 2, y, line, body); + + y += bodyHeight; + body.foreground = 0x07E0; + surface.drawText(surface.width() / 2, y, "Watch the LCD unit", body); + + gfx::TextStyle hint; + hint.font = &gfx::FONT_5X7; + hint.foreground = 0x8410; + hint.align = gfx::TextAlign::Center; + hint.verticalAlign = gfx::TextVerticalAlign::Bottom; + hint.scale = static_cast(bodyScale); + surface.drawText(surface.width() / 2, surface.height() - 6, "Any input to exit", hint); + }, gfx::Prepare::Overwrite); + + if (primary.info().slowRefresh) { + primary.refresh(); + } +} + +// Everything outside the play area is static, so it is drawn once with the unit's own primitives +// instead of going through the sink's frame buffer. +void drawLcdChrome(UnitLcd& lcd, int playHeight) { + const int footerY = LCD_PLAY_TOP + playHeight + 8; + + lcd.fillScreen(LCD_BACKGROUND); + lcd.drawText(6, 4, "UnitLCD Gfx Sink", LCD_TITLE_COLOR, LCD_BACKGROUND, 2); + lcd.drawText(6, 23, "SinkTarget + UnitLcdSink", LCD_SUBTITLE_COLOR, LCD_BACKGROUND); + lcd.fillRect(6, 34, static_cast(lcd.width() - 7), 34, LCD_CHROME); + lcd.drawRect( + static_cast(LCD_PLAY_LEFT - 1), + static_cast(LCD_PLAY_TOP - 1), + static_cast(LCD_PLAY_WIDTH + 2), + static_cast(playHeight + 2), + LCD_CHROME + ); + lcd.drawText(6, static_cast(footerY), "Any input to exit", 0x8410, LCD_BACKGROUND); +} + +void drawLcdFailure(UnitLcd& lcd) { + lcd.fillScreen(LCD_BACKGROUND); + lcd.drawText(6, 6, "Sink init failed", 0xFFFF, LCD_BACKGROUND, 2); + lcd.drawText(6, 28, "Not enough contiguous RAM", LCD_SUBTITLE_COLOR, LCD_BACKGROUND); + lcd.drawText(6, 38, "for the play area frame", LCD_SUBTITLE_COLOR, LCD_BACKGROUND); + lcd.drawText(6, 120, "Any input to exit", 0x8410, LCD_BACKGROUND); +} + +} // namespace + +void runApplication(Device* display, AnyInput* input) { + struct TaskEventGroup eventGroup {}; + task_event_group_construct(&eventGroup); + struct AppEventSubscription subscription {}; + check(app_event_subscribe(&subscription, &eventGroup) == ERROR_NONE); + + Device* i2c = findGroveI2cDevice(); + if (i2c == nullptr) { + gfx::Target primary(display); + if (primary.valid()) { + preparePrimary(primary); + showPrimaryMessage(primary, "UnitLCD Gfx Sink", "No Grove I2C port found", "Set a Grove port to I2C"); + } else { + ESP_LOGE(TAG, "Primary display can't be used for the status screen"); + } + waitForExit(subscription, *input); + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&eventGroup); + return; + } + + UnitLcd lcd; + UnitPaHub hub; + uint8_t channel = UnitPaHub::NO_CHANNEL; + bool usingHub = false; + bool found = false; + + if (hub.begin(i2c)) { + for (uint8_t candidate = 0; candidate < UnitPaHub::NUM_CHANNELS && !found; candidate++) { + if (!hub.select(candidate)) { + continue; + } + if (lcd.begin(i2c)) { + channel = candidate; + usingHub = true; + found = true; + } + } + if (!found) { + hub.deselect(); + } + } + + if (!found && lcd.begin(i2c)) { + usingHub = false; + found = true; + } + + if (!found) { + gfx::Target primary(display); + if (primary.valid()) { + preparePrimary(primary); + showPrimaryMessage(primary, "UnitLCD Gfx Sink", "LCD unit not found", "Check the Grove cable and address"); + } else { + ESP_LOGE(TAG, "Primary display can't be used for the status screen"); + } + waitForExit(subscription, *input); + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&eventGroup); + return; + } + + lcd.setBrightness(180); + lcd.setRotation(1); // landscape, 240x135 + + // Allocate the play-area frame first: on no-PSRAM boards the primary Target may not be able to + // coexist with it, and a partial frame fits where a full one does not. If even the preferred + // height doesn't fit in one contiguous block, try smaller ones. +#ifdef ESP_PLATFORM + ESP_LOGI( + TAG, + "Largest free blocks: internal %u bytes, any %u bytes", + static_cast(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), + static_cast(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)) + ); +#endif + + UnitLcdSink sink(lcd, LCD_PLAY_LEFT, LCD_PLAY_TOP); + std::optional lcdTarget; + lcdTarget.emplace(sink, LCD_PLAY_WIDTH, LCD_PLAY_HEIGHT, DISPLAY_COLOR_FORMAT_RGB565); + for (std::size_t i = 0; !lcdTarget->valid() && i < std::size(LCD_PLAY_HEIGHT_FALLBACKS); i++) { + lcdTarget.reset(); + lcdTarget.emplace(sink, LCD_PLAY_WIDTH, LCD_PLAY_HEIGHT_FALLBACKS[i], DISPLAY_COLOR_FORMAT_RGB565); + } + + if (!lcdTarget->valid()) { + drawLcdFailure(lcd); + tt::kernel::delayTicks(tt::kernel::millisToTicks(80)); + lcd.setBrightness(0); + if (usingHub) { + hub.deselect(); + } + ESP_LOGE(TAG, "LCD sink unavailable; see the gfx/UnitLcdSink log for the reason"); + waitForExit(subscription, *input); + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&eventGroup); + return; + } + + const int playHeight = lcdTarget->info().height; + const int footerY = LCD_PLAY_TOP + playHeight + 8; + + // The unit's framebuffer survives app restarts, so erase the previous run before drawing. + drawLcdChrome(lcd, playHeight); + + ESP_LOGI( + TAG, + "UnitLCD ready: %dx%d (%s), play area %dx%d at (%d, %d)", + lcd.width(), + lcd.height(), + usingHub ? "via PaHub" : "direct", + LCD_PLAY_WIDTH, + playHeight, + LCD_PLAY_LEFT, + LCD_PLAY_TOP + ); + if (playHeight != LCD_PLAY_HEIGHT) { + ESP_LOGW( + TAG, + "Reduced the play area to %dx%d to fit the free internal RAM", + LCD_PLAY_WIDTH, + playHeight + ); + } + + gfx::Target primary(display); + if (primary.valid()) { + preparePrimary(primary); + drawPrimaryStatus(primary, lcd.width(), lcd.height(), usingHub, channel); + } else { + ESP_LOGW(TAG, "Not enough memory for the primary status target; only the LCD is active"); + } + + // All coordinates below are local to the sink's play area. + const int radius = LCD_BALL_RADIUS; + const int ballMinX = radius + 2; + const int ballMaxX = LCD_PLAY_WIDTH - radius - 2; + const int ballMinY = radius + 2; + const int ballMaxY = playHeight - radius - 2; + + int ballX = LCD_PLAY_WIDTH / 2; + int ballY = playHeight / 2; + int velocityX = 3; + int velocityY = 2; + int previousX = ballX; + int previousY = ballY; + unsigned long frames = 0; + bool clearPlayArea = true; + + auto drawLcd = [&](gfx::Surface& surface) { + if (clearPlayArea) { + surface.clear(0x0000); + clearPlayArea = false; + } else { + surface.fillRect( + previousX - radius, + previousY - radius, + radius * 2 + 1, + radius * 2 + 1, + 0x0000 + ); + } + surface.fillCircle(ballX, ballY, radius, LCD_BALL_COLOR); + }; + + while (true) { + if (usingHub && !hub.select(channel)) { + ESP_LOGW(TAG, "PaHub channel %d became unavailable", channel); + break; + } + + lcdTarget->render(drawLcd); + + // The frame counter is outside the play area, so draw it straight onto the unit. + char status[16]; + const int length = std::min( + std::snprintf(status, sizeof(status), "f:%lu", frames), + static_cast(sizeof(status)) - 1 + ); + lcd.drawText( + static_cast(lcd.width() - 6 - length * 6), + static_cast(footerY), + status, + 0x07E0, + LCD_BACKGROUND + ); + + frames++; + previousX = ballX; + previousY = ballY; + ballX += velocityX; + ballY += velocityY; + if (ballX <= ballMinX) { + ballX = ballMinX; + velocityX = std::abs(velocityX); + } else if (ballX >= ballMaxX) { + ballX = ballMaxX; + velocityX = -std::abs(velocityX); + } + if (ballY <= ballMinY) { + ballY = ballMinY; + velocityY = std::abs(velocityY); + } else if (ballY >= ballMaxY) { + ballY = ballMaxY; + velocityY = -std::abs(velocityY); + } + + if (takeExit(subscription, *input)) { + break; + } + tt::kernel::delayTicks(tt::kernel::millisToTicks(40)); + } + + ESP_LOGI( + TAG, + "Stopped after %lu frames; last frame had %d dirty rect(s), %d pixel(s)", + frames, + lcdTarget->stats().dirtyRects, + lcdTarget->stats().dirtyPixels + ); + + lcd.fillScreen(0x0000); + tt::kernel::delayTicks(tt::kernel::millisToTicks(80)); + lcd.setBrightness(0); + if (usingHub) { + hub.deselect(); + } + + check(app_event_unsubscribe(&subscription) == ERROR_NONE); + task_event_group_destruct(&eventGroup); +} diff --git a/Apps/UnitLcdGfxDemo/main/Source/GroveLookup.cpp b/Apps/UnitLcdGfxDemo/main/Source/GroveLookup.cpp new file mode 100644 index 0000000..3ff462c --- /dev/null +++ b/Apps/UnitLcdGfxDemo/main/Source/GroveLookup.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "GroveLookup.h" + +#include + +namespace { + +struct GroveChildSearch { + Device* result; +}; + +bool onGroveDevice(Device* groveDevice, void* context) { + auto* search = static_cast(context); + + GroveMode mode; + if (grove_get_mode(groveDevice, &mode) != ERROR_NONE) { + return true; + } + if (mode != GROVE_MODE_I2C) { + return true; + } + + device_for_each_child(groveDevice, search, [](Device* child, void* ctx) -> bool { + auto* state = static_cast(ctx); + if (device_get_type(child) == &I2C_CONTROLLER_TYPE && device_is_ready(child)) { + state->result = child; + return false; + } + return true; + }); + + return search->result == nullptr; +} + +} // namespace + +Device* findGroveI2cDevice() { + GroveChildSearch search { nullptr }; + device_for_each_of_type(&GROVE_TYPE, &search, onGroveDevice); + return search.result; +} diff --git a/Apps/UnitLcdGfxDemo/main/Source/Main.cpp b/Apps/UnitLcdGfxDemo/main/Source/Main.cpp new file mode 100644 index 0000000..0b739f5 --- /dev/null +++ b/Apps/UnitLcdGfxDemo/main/Source/Main.cpp @@ -0,0 +1,95 @@ +#include "Application.h" + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +constexpr auto TAG = "UnitLcdGfxDemo"; + +// Shows a blocking error dialog and waits for it to close before the caller finishes this app. +static void showErrorAndWait(AppInstanceId appInstanceId, const char* message) { + const char* argv[] = { "Error", message, "OK" }; + uint32_t dialogInstanceId = 0; + app_start_for_result("tactility.alertdialog", 3, argv, appInstanceId, &dialogInstanceId); + if (dialogInstanceId == 0) { + return; + } + + struct TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + struct AppEventSubscription sub {}; + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + + while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool done = false; + struct AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + done = true; + break; + } + if (event.type == APP_EVENT_RESULT && event.result.launch_id == dialogInstanceId) { + app_manager_stop(dialogInstanceId); + done = true; + break; + } + } + if (done) break; + } + + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); +} + +extern "C" { + +int main(int argc, char* argv[]) { + AppInstanceId app_instance_id = app_scheduler_current_app_id(); + + struct Device* display_device; + if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) { + ESP_LOGE(TAG, "No display device found"); + showErrorAndWait(app_instance_id, "No display device was found."); + return 0; + } + + auto input = new AnyInput(); + if (!input->available()) { + delete input; + showErrorAndWait(app_instance_id, "No input device was found."); + return 0; + } + + // The demo uses the primary display directly while the LCD unit is driven through a sink. + // Target creation is deferred into runApplication() so the LCD frame can be allocated first. + module_stop(&lvgl_module); + + runApplication(display_device, input); + + device_put(display_device); + delete input; + + if (!module_is_started(&lvgl_module)) { + module_start(&lvgl_module); + } + + return 0; +} + +} diff --git a/Apps/UnitLcdGfxDemo/manifest.properties b/Apps/UnitLcdGfxDemo/manifest.properties new file mode 100644 index 0000000..054224e --- /dev/null +++ b/Apps/UnitLcdGfxDemo/manifest.properties @@ -0,0 +1,11 @@ +manifest.version=0.3 +target.sdk=0.8.0-dev +target.platforms=esp32s3,esp32p4 +id=tactility.unitlcdgfxdemo +version.name=0.1.0 +version.code=1 +name=UnitLCD Gfx Demo +description=Drives an M5Stack LCD Unit through the Gfx SinkTarget/UnitLcdSink path while the primary display shows status. +app.0.id=tactility.unitlcdgfxdemo +app.0.name=UnitLCD Gfx Demo +app.0.binary=unitlcdgfxdemo diff --git a/Libraries/Gfx/DESIGN.md b/Libraries/Gfx/DESIGN.md new file mode 100644 index 0000000..eace9e9 --- /dev/null +++ b/Libraries/Gfx/DESIGN.md @@ -0,0 +1,327 @@ +# Gfx design + +This is the working design for turning `Libraries/Gfx` from a direct-to-display presenter into a +general 2D drawing library. It is deliberately written down so the API can be reviewed before the +larger pieces (images, canvases, extra fonts) are built. + +Status: steps 1-4 are implemented (public `Surface` with clipping, primitives, fonts/text, +blitting, transparency, masks, `Canvas`, polygons, alpha blending, an 8x16 body font, +proportional/layout improvements, and the generic Sink/SinkTarget presentation path). Next: step 5 +(pixel_buffer helpers, after a discussion with ByteWelder). + +## Goals + +- One drawing API that works on every panel `gfx::Target` supports: MIPI DSI, RGB, SPI, e-paper, + monochrome, grayscale, and panels with or without PSRAM. +- Enough primitives and text to build a small UI without LVGL. +- Usable off-screen: the same `Surface` code draws into an owned buffer, a sprite, or a frame + buffer, not only inside `Target::render()`. +- No heap allocation in the drawing path. Fonts and geometry live in flash/stack. +- Keep `Target` responsible for presentation policy and `Surface` responsible for pixels. + +## Non-goals + +- Widgets, layout engines, an event loop, animation timelines. +- A full vector/raster engine (anti-aliasing, Bézier curves, transforms) in the current steps. +- Replacing LVGL. This is for apps that deliberately render outside LVGL. + +## Layers + +``` + app drawing code + | + gfx::Surface <- primitives, text, clip, dirty tracking + | + PixelBuffer (graphics-module) + | <- storage, format conversion, cache sync + gfx::Target <- flip / full-frame / banded / PPA transform + | + display driver +``` + +- `PixelBuffer` is the storage primitive. It knows the format, stride and cache maintenance. + `gfx::Surface` is a view over one. A `Surface` never owns the buffer. +- `Target` decides how a frame reaches the panel. It creates one `Surface` per frame (or one per + band) and hands it to the render callback. +- Drawing code only sees `Surface`, so the same code works in flip, full-frame and banded modes. + +## Coordinate model + +- All coordinates are **world coordinates** in the display's pixel space. +- A `Surface` has `bounds()`: the world rectangle it covers. In banded mode that is + `{0, bandY, width, bandY + bandRows}`. +- A `Surface` also has a **clip rectangle**, defaulting to `bounds()`. Every primitive is clipped to + it. `setClip()` lets an app restrict drawing to a panel sub-region or a scroll viewport. +- A public `Surface(PixelBuffer*)` covers the whole buffer with its origin at `(0, 0)`. A + `Surface(PixelBuffer*, Rect view)` is a sub-view of a larger buffer; its drawing coordinates + start at the view's top-left. +- All drawing is clipped, never wrapped or reflected. + +## Dirty tracking + +`Target` presents per dirty region. `Surface` owns the `DirtyRegions` set for the current frame: + +- Simple primitives (`fillRect`, `line`, `pixel`, ...) clip first, then mark the visible rectangle. +- Compound primitives (circle, triangle, text, ...) mark their bounding box **once**, then draw + their spans without touching the dirty set. This matters because `DirtyRegions` has a small + fixed capacity; one entry per glyph or per scanline would collapse into a whole-frame present. +- `DirtyRegions::add()` merges the new rectangle into every existing rectangle it intersects, so a + clear plus a redraw usually collapses back to one entry. + +Apps that write through a raw pointer must call `markDirty()` themselves. + +`Surface::pixel()` marks its pixel. Internal span/line helpers do not; that is what keeps compound +primitives cheap. + +## Fonts and text + +Text is data-driven, not hard-coded to one font. + +```cpp +struct Font { + uint8_t glyphWidth; // glyph cell width in pixels + uint8_t glyphHeight; // glyph cell height in pixels + uint8_t advance; // horizontal step at scale 1 (glyphWidth + gap) + uint8_t lineHeight; // vertical step at scale 1 + uint8_t firstChar; + uint8_t lastChar; + FontLayout layout; // Rows (bit 7 = left), RowsLsb (bit 0 = left) or Columns (bit 0 = top) + const uint8_t* data; // packed 1bpp glyphs +}; +``` + +- Fonts are selected at runtime by passing a `const Font&`. Apps can supply their own font data + without changing the library. `FONT_5X7` uses `Columns`; the public-domain `FONT_8X8` uses + `RowsLsb` because its source stores bit 0 as the leftmost pixel. `FONT_8X16` is Terminus + (OFL-1.1), and `FONT_5X7_PROPORTIONAL` shares the 5x7 glyph data with per-glyph widths/advances. +- Sizes come from two mechanisms: native fonts (5x7, 8x8, 8x16) and integer scaling + (`TextStyle::scale`). Small text should use a native font; large display text can be scaled + (8x16 at scale 2 is 16x32). +- `TextStyle` carries font, foreground, optional background, scale, tab width, ellipsis, + horizontal/vertical alignment and wrapping. `Font` can carry optional per-glyph width/advance + tables for proportional fonts. +- Text is drawn as horizontal spans (runs of set bits), not one `writePixel()` per pixel. That makes + it fast on RGB565 and still correct on grayscale/monochrome, because spans go through the same + format conversion as `fillRect()`. +- Dirty tracking marks the whole text block once. +- The text pipeline treats bytes as characters. The 5x7/8x8 fonts are ASCII; the 8x16 Terminus + font also has Latin-1 glyphs. A UTF-8 decoder can be added later without changing the `Font` + structure (glyphs would be indexed by code point; the current fonts are single-byte). + +Example: + +```cpp +#include + +gfx::Target target(display_device); + +target.render([](gfx::Surface& s) { + s.clear(0x0000); + s.fillRoundRect(8, 8, 200, 60, 10, 0x000F); + + // Transparent text: only the glyph pixels are written. + s.drawText(20, 20, "Hello", gfx::FONT_8X8, 0xFFFF); + + // Opaque text with a background and a larger glyph. + gfx::TextStyle style; + style.font = &gfx::FONT_5X7; + style.foreground = 0xF800; + style.background = 0x0000; + style.opaque = true; + style.scale = 2; + s.drawText(20, 90, "BIG", style); +}); +``` + +## Device-agnostic presentation + +`Target` is tied to a Tactility display `Device`. The drawing layer is not: + +- `Surface(PixelBuffer*)` works on any buffer, including one an app allocates itself. +- Apps that drive a non-display panel (an I2C LCD unit, a headless bitmap, an image export) can + allocate a `PixelBuffer`, draw into it with `Surface`, and push the bytes themselves. + +The generic presentation path is implemented by `SinkTarget`, which is not tied to a display +`Device`: + +- `Sink` has `begin(SinkInfo)`, `present(PixelBuffer&, Rect)`, `end()` and an optional `refresh()`. + `frame_buffer_policy()` lets a backend ask for DMA-capable memory. +- `SinkTarget` owns one full-frame `PixelBuffer`, exposes the usual `Info`/`Stats` and render + callback, and presents dirty regions to the sink. The buffer persists between frames. +- `DisplaySink` adapts a Tactility display device (whole-row `display_draw_bitmap()`); `gfx::Target` + remains the optimized display path with flip/banded/PPA modes. +- `UnitLcdSink` adapts an M5Stack LCD Unit over I2C; it lives in `Libraries/UnitLcdGfx`, outside + `M5UnitModules`. It takes an origin, so it can present a sub-frame while static chrome is drawn + with the unit's own primitives; smaller frames fit fragmented heaps on no-PSRAM boards. + +## Phases + +Order: **3 (fonts/layout) → 4 (presentation sinks) → 5 (pixel_buffer helpers)**. Steps 3 and 4 stay +inside `Gfx`/new Gfx-side code; step 5 is deliberately last because it changes +`TactilityWork/Modules/graphics-module` and requires a firmware rebuild. The helper set in step 5 is +a proposal to discuss with ByteWelder before starting it. Fonts/layout goes first because it is +self-contained and improves every screen; sinks follow because they are the last Gfx-only +architectural change and their present path can inform the pixel_buffer discussion. + +### Step 1 - foundation (done) + +- Public `Surface(PixelBuffer*)` and sub-view constructor. +- Bounds + clip rectangle. +- `hline`, `vline`, `drawRect`, `fillCircle`, `drawCircle`, `fillEllipse`, `drawEllipse`, + `fillRoundRect`, `drawRoundRect`, `fillTriangle`, `drawTriangle`, `fillArc`, `drawArc`, + `gradient`. +- `Font`, `TextStyle`, `drawText`, `measureText`, 5x7 and 8x8 fonts. +- Improved dirty-region merging. +- Updated README. + +### Step 2 - images and composition (done) + +- `blit()` with clipping and format conversion, plus a same-format row-copy fast path. +- `blitTransparent()` for colour-key transparency. +- `drawMask()` for 1bpp icon masks (MSB- or LSB-first) and `drawAlphaMask()` for 8bpp alpha masks. +- `Canvas`: an owned `PixelBuffer` + `Surface` for off-screen composition and sprites. +- `blendPixel()` for RGB565 alpha blending. +- `drawPolygon()` / `fillPolygon()` (even-odd scanline fill, up to 32 vertices). +- The existing Material Symbols data in apps is 2bpp LVGL font data rather than 1bpp masks, so a + 2bpp glyph decoder remains a future addition; the 1bpp/8bpp paths are independent of it. + +### Step 3 - more fonts and layout (done) + +- Native 8x16 Terminus font (`FONT_8X16`), Latin-1 0x20-0xFF, OFL-1.1 (license in + `Licenses/Terminus-OFL.txt`). `scale = 2` gives a 16x32 cell for large display text. +- Optional per-glyph widths and advances on `Font`, plus the built-in `FONT_5X7_PROPORTIONAL`. +- Configurable tab width (`TextStyle::tabSize`), ellipsis truncation for boxed text + (`TextStyle::ellipsis`), and wrap/measurement that understands proportional advances. +- A native 12x16 font is deferred until a suitably licensed bitmap dataset is chosen; 8x16 at + scale 2 covers the 16x32 case for now. +- 9-slice panels, progress bars and gauges stay app-level for now; `Canvas`/`blit` provide the + building blocks if they move into the library later. + +### Step 4 - presentation sinks (done) + +- `Sink`/`SinkInfo`/`SinkTarget`: a display-independent target that owns one full-frame + `PixelBuffer`, tracks dirty regions, and presents them to a backend. The buffer persists between + frames; `Prepare` is accepted for API symmetry but has no effect. +- `Sink::frame_buffer_policy()` lets backends request DMA-capable memory instead of the default + external-RAM preference (used by `DisplaySink` when the panel may DMA directly). +- `DisplaySink` adapter: presents whole rows through `display_draw_bitmap()`, valid for every + display format including monochrome. `gfx::Target` remains the optimized display path + (flip/full-frame/banded/PPA transform). `Target::release()` frees its render buffers while the + panel keeps the last frame, for apps that need the memory for another target. +- `UnitLcdSink` in `Libraries/UnitLcdGfx`, outside `M5UnitModules`: sets the LCD window and streams + changed rows with `UnitLcd::writePixels()`. +- Optional LVGL canvas bridge remains future work. + +### Step 5 - pixel_buffer helpers (graphics-module, deferred) + +Deferred until after a discussion with ByteWelder. It requires a change to +`TactilityWork/Modules/graphics-module` and its module symbol table, so it needs a firmware rebuild; +`Surface` keeps its own implementations until then. Proposed helpers: + +- `pixel_buffer_fill_rect()` +- `pixel_buffer_hline()` / `pixel_buffer_vline()` +- clipped `pixel_buffer_blit()` +- `pixel_buffer_wrap_offset()` for sub-rects without pointer arithmetic +- `pixel_buffer_copy()` for same-format rect copies + +Points to settle with ByteWelder before starting it: + +- Which of these are shared enough to belong in the module rather than `Gfx`? +- Is adding entries to the module symbol table acceptable, and is there a version/ABI concern? +- Should `pixel_buffer_blit()` become clipped by default (behavior change) or gain a new variant? +- Should sub-view/offset support live in `pixel_buffer` or stay a `Surface` concern? +- Which changes also help the LVGL path, so both consumers benefit from the module work? + +## Relationship to `pixel_buffer` + +ByteWelder's suggestion ("maybe `Surface.h` should wrap `pixel_buffer`") is already true +internally: `Surface` stores the buffer pointer and uses `pixel_buffer_get_*` for data, stride, +width, height and format. The change in step 1 is to make that relationship public and to stop +requiring a `Target` to create a `Surface`. + +The remaining question is how much to push down into `pixel_buffer`. My recommendation is to add +only operations that are useful to more than one consumer (fill, spans, clipped blit, sub-views) +and keep composition (circles, text, arcs) in `Gfx`. That keeps the module small and avoids +rebuilding the module every time a drawing primitive is added. This is the step 5 proposal; it is +deferred until after a discussion with ByteWelder and is not part of the current Gfx-only work. + +## Performance notes + +- `fillRect` on RGB565/BGR565 uses `std::fill` per row after converting the color once. +- Compound shapes draw horizontal spans, so they touch each row once. +- Text draws runs of set bits, so a glyph row costs one span per run rather than one call per pixel. +- Same-format blits copy whole rows with `memcpy`; transparent blits over a fast RGB565 format + compare native pixels directly. Cross-format blits go through RGB565 conversion per pixel. +- Polygons mark their bounding box once and fill each scanline with spans. +- Arcs are the expensive primitive: filled arcs test pixels in the bounding box. They are fine for + small displays and indicators, not for full-screen animation. A scanline arc is a future + optimisation. +- In banded mode the callback runs once per band. Do not mutate app state in it. +- In flip/full-frame mode the buffer persists. Clear a region before drawing into it again, or use + `Prepare::Overwrite` and make the callback repaint everything it changes. + +### Symbol export notes + +Apps are loaded against the firmware's export table, so a missing libc/libm symbol fails at load +time. `Scripts/check-app-symbols.py` reports these. Current workarounds that should be replaced when +the firmware exports the real symbol (or accepted permanently if not worth a firmware change): + +- `Polygon.cpp` uses a local `roundToInt()` because `lroundf` (from `std::lround(float)`) is not + exported. Replace with `std::lround()`/`lroundf()` once `lroundf` is in the c-symbols module. +- App ELFs do not define `__dso_handle`, so function-local static objects with destructors cannot be + used (a `static gfx::Canvas` produced an undefined `__dso_handle` at link time). The benchmark + creates the sprite `Canvas` as an automatic object in `runApplication()` and passes it through + `Context` instead. This is a linkage limitation, not a missing export-table entry, but worth + knowing when adding assets to direct-rendering apps. +- App-local display drivers need the firmware to export ESP-IDF's SPI entry points. Linking IDF + driver archives (`ELF_COMPONENTS`) into the app ELF does not work: their objects reference + peripheral base addresses that only the firmware's linker script defines (`GPIO`, `GPSPI2`, + `IO_MUX`, ...), so the load fails on those data symbols. `platform-esp32` now exports + `spi_bus_initialize`, `spi_bus_free`, `spi_bus_add_device`, `spi_bus_remove_device`, + `spi_device_polling_transmit`, `spi_device_acquire_bus` and `spi_device_release_bus`; see + `Apps/MystifySpiDemo/ili9341-panel-module`, which also contains the driver itself. +- App code must not be called from an ISR that may run with caches disabled: app ELFs can be + loaded into PSRAM (`CONFIG_ELF_LOADER_LOAD_PSRAM`), while the firmware builds with + `CONFIG_SPI_MASTER_ISR_IN_IRAM`. That is why the ILI9341 driver uses synchronous + `spi_device_polling_transmit()` transactions and no completion callback; an `esp_lcd`-based + driver needs a completion signal that does not execute app code in that window. + +`std::cos`/`std::sin` (double), `memcpy`, `snprintf`, `operator new` and the graphics-module calls +all resolve against the current firmware. + +## File map + +``` +Include/gfx/ + Gfx.h umbrella header + Rect.h rectangle/point maths + DirtyRegions.h damage set + Surface.h drawing surface + Canvas.h owned PixelBuffer + Surface + Sink.h Sink interface and SinkTarget + DisplaySink.h display_draw_bitmap() sink adapter + Font.h font descriptor, text styles, font declarations + Text.h text measurement helpers + Target.h display presentation policy +Source/ + Surface.cpp core, clipping, rects, lines, gradients + Shapes.cpp circles, ellipses, rounded rects, triangles, arcs + Images.cpp blit, transparency, masks, alpha blending + Polygon.cpp polygon outline and scanline fill + Text.cpp text layout and rendering + Fonts.cpp built-in 5x7/8x8/proportional data and measurement + FontTerminus16.cpp 8x16 Terminus font data (OFL-1.1) + Canvas.cpp canvas allocation + SinkTarget.cpp generic sink presentation and dirty regions + DisplaySink.cpp display device sink adapter + ColorUtils.h internal colour/format helpers + Target.cpp display presentation +Licenses/ + Terminus-OFL.txt OFL-1.1 text for the 8x16 font data +``` + +`Libraries/UnitLcdGfx` contains the separate `UnitLcdSink` adapter; it is not part of the Gfx +library and does not modify `M5UnitModules`. + +Apps include `Libraries/Gfx/Include` and compile `Libraries/Gfx/Source/*.c*` through their existing +CMake `GLOB`; there is no separate library build. diff --git a/Libraries/Gfx/Include/gfx/Canvas.h b/Libraries/Gfx/Include/gfx/Canvas.h new file mode 100644 index 0000000..0e38c95 --- /dev/null +++ b/Libraries/Gfx/Include/gfx/Canvas.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "Surface.h" + +#include +#include + +namespace gfx { + +/** + * An owned PixelBuffer plus a Surface over it. + * + * Useful for off-screen composition: draw a sprite once, then blit it into a Target surface. + * The buffer is allocated with pixel_buffer_create() (external RAM preferred, 64-byte aligned) + * unless an explicit MemoryPolicy is supplied. A Canvas never allocates in the drawing path. + * + * gfx::Canvas sprite(32, 32, DISPLAY_COLOR_FORMAT_RGB565); + * if (sprite.valid()) { + * sprite.surface().clear(0xF81F); // colour key + * sprite.surface().fillCircle(16, 16, 12, 0x07FF); + * } + * target.render([&](gfx::Surface& s) { + * s.blitTransparent(*sprite.buffer(), 0, 0, 32, 32, x, y, 0xF81F); + * }); + */ +class Canvas { +public: + Canvas(int width, int height, enum DisplayColorFormat format = DISPLAY_COLOR_FORMAT_RGB565, const MemoryPolicy* policy = nullptr); + + ~Canvas(); + + Canvas(const Canvas&) = delete; + Canvas& operator=(const Canvas&) = delete; + Canvas(Canvas&&) = delete; + Canvas& operator=(Canvas&&) = delete; + + bool valid() const { return surface_.valid(); } + + Surface& surface() { return surface_; } + + const Surface& surface() const { return surface_; } + + struct PixelBuffer* buffer() { return buffer_; } + + const struct PixelBuffer* buffer() const { return buffer_; } + + int width() const { return surface_.width(); } + + int height() const { return surface_.height(); } + + enum DisplayColorFormat format() const { return surface_.format(); } + + void clear(uint16_t rgb565 = 0x0000) { surface_.clear(rgb565); } + +private: + struct PixelBuffer* buffer_ = nullptr; + Surface surface_; +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Include/gfx/DirtyRegions.h b/Libraries/Gfx/Include/gfx/DirtyRegions.h new file mode 100644 index 0000000..c3b69ba --- /dev/null +++ b/Libraries/Gfx/Include/gfx/DirtyRegions.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "Rect.h" + +namespace gfx { + +/** + * A small set of changed rectangles. + * + * New rectangles are merged into every rectangle they intersect, so a frame that clears an object's + * bounding box and then draws primitives inside it collapses back into that single rectangle + * instead of one entry per primitive. When the set is full, further rectangles are merged into the + * last entry. + */ +class DirtyRegions { +public: + static constexpr int CAPACITY = 8; + + void clear() { count_ = 0; } + + int count() const { return count_; } + + Rect at(int index) const { return rects_[index]; } + + void add(Rect rect) { + if (rect.empty()) { + return; + } + + // Merge with every rectangle this one touches, then re-check the merged result: growing it + // can make it overlap entries that the original didn't. + int i = 0; + while (i < count_) { + if (rects_[i].intersects(rect)) { + rect = rects_[i].unite(rect); + rects_[i] = rects_[--count_]; + i = 0; + } else { + i++; + } + } + + if (count_ < CAPACITY) { + rects_[count_++] = rect; + } else { + rects_[CAPACITY - 1] = rects_[CAPACITY - 1].unite(rect); + } + } + +private: + Rect rects_[CAPACITY] {}; + int count_ = 0; +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Include/gfx/DisplaySink.h b/Libraries/Gfx/Include/gfx/DisplaySink.h new file mode 100644 index 0000000..86703ed --- /dev/null +++ b/Libraries/Gfx/Include/gfx/DisplaySink.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "Sink.h" + +namespace gfx { + +/** + * A Sink that presents to a Tactility display Device with display_draw_bitmap(). + * + * This is the generic full-frame path: it presents whole rows, so it works with every display + * colour format, including monochrome. For flip/full-frame/banded policy and hardware + * rotation/scaling, use gfx::Target instead. + */ +class DisplaySink : public Sink { +public: + /** Takes a reference on the device until destruction. */ + explicit DisplaySink(struct Device* display); + + ~DisplaySink() override; + + DisplaySink(const DisplaySink&) = delete; + DisplaySink& operator=(const DisplaySink&) = delete; + + bool begin(const SinkInfo& info) override; + + bool present(const struct PixelBuffer& buffer, const Rect& rect) override; + + void end() override; + + bool frame_buffer_policy(MemoryPolicy& out) const override; + + error_t refresh() override; + +private: + struct Device* display_ = nullptr; + SinkInfo info_ {}; + bool begun_ = false; +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Include/gfx/Font.h b/Libraries/Gfx/Include/gfx/Font.h new file mode 100644 index 0000000..4c9f95e --- /dev/null +++ b/Libraries/Gfx/Include/gfx/Font.h @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +namespace gfx { + +/** How a font's 1bpp glyph rows are packed. */ +enum class FontLayout { + /** One byte per row, most significant bit is the leftmost pixel. */ + Rows, + /** One byte per row, least significant bit is the leftmost pixel (the public-domain 8x8 font). */ + RowsLsb, + /** One byte per column, least significant bit is the top pixel (the classic 5x7 layout). */ + Columns, +}; + +/** + * A bitmap font. + * + * Glyphs are stored contiguously for characters [firstChar, lastChar]. A glyph occupies + * ceil(glyphWidth / 8) * glyphHeight bytes, in the order described by `layout`. Fonts are constant + * data and are never allocated. + * + * Native fonts are used for small text; `TextStyle::scale` magnifies them for larger text. Scaling a + * small font is cheap and crisp enough at integer scales, but a native font at the target size looks + * better for body text. + */ +struct Font { + uint8_t glyphWidth = 0; + uint8_t glyphHeight = 0; + /** Horizontal advance at scale 1, including the gap to the next glyph. */ + uint8_t advance = 0; + /** Vertical advance at scale 1, including the line gap. */ + uint8_t lineHeight = 0; + uint8_t firstChar = 0; + uint8_t lastChar = 0; + FontLayout layout = FontLayout::Rows; + const uint8_t* data = nullptr; + /** + * Optional per-glyph visual width for [firstChar, lastChar]. Null means glyphWidth. + * Values must not exceed glyphWidth; columns beyond the value are ignored. + */ + const uint8_t* glyphWidths = nullptr; + /** + * Optional per-glyph advance for [firstChar, lastChar]. Null means advance. + * This is what makes a fixed-cell font proportional. + */ + const uint8_t* advances = nullptr; + + bool contains(char c) const { + const auto value = static_cast(c); + return data != nullptr && value >= firstChar && value <= lastChar; + } + + const uint8_t* glyph(char c) const { return contains(c) ? data + (static_cast(c) - firstChar) * bytesPerGlyph() : nullptr; } + + /** Visible columns for this glyph (at most glyphWidth). */ + uint8_t widthFor(char c) const { + const auto value = static_cast(c); + if (glyphWidths == nullptr || value < firstChar || value > lastChar) { + return glyphWidth; + } + const uint8_t width = glyphWidths[value - firstChar]; + return width > glyphWidth ? glyphWidth : width; + } + + /** Horizontal step for this glyph at scale 1. */ + uint8_t advanceFor(char c) const { + const auto value = static_cast(c); + if (advances == nullptr || value < firstChar || value > lastChar) { + return advance; + } + return advances[value - firstChar]; + } + + int bytesPerGlyph() const { + if (layout == FontLayout::Rows || layout == FontLayout::RowsLsb) { + return ((glyphWidth + 7) / 8) * glyphHeight; + } + return glyphWidth * ((glyphHeight + 7) / 8); + } +}; + +/** 5x7 glyphs, ASCII 32-126, classic column-major layout. */ +extern const Font FONT_5X7; + +/** 5x7 glyphs with per-glyph widths and advances (proportional variant). */ +extern const Font FONT_5X7_PROPORTIONAL; + +/** 8x8 glyphs, ASCII 0-127, row-major layout. Public-domain VGA-style font. */ +extern const Font FONT_8X8; + +/** 8x16 Terminus glyphs, Latin-1 32-255. OFL-1.1; see Licenses/Terminus-OFL.txt. */ +extern const Font FONT_8X16; + +enum class TextAlign { + Left, + Center, + Right, +}; + +enum class TextVerticalAlign { + Top, + Middle, + Bottom, +}; + +enum class TextWrap { + None, + /** Wrap at spaces where possible, otherwise between characters. */ + Word, +}; + +/** Everything needed to draw a run of text. */ +struct TextStyle { + const Font* font = &FONT_5X7; + /** Glyph colour, RGB565. */ + uint16_t foreground = 0xFFFF; + /** Cell background, RGB565. Only used when `opaque` is true. */ + uint16_t background = 0x0000; + /** When false, only glyph pixels are written and the background is left untouched. */ + bool opaque = false; + /** Integer magnification. 1 draws the font at its native size. */ + uint8_t scale = 1; + /** Number of cells a tab advances in drawText()/measureText(). */ + uint8_t tabSize = 4; + /** When true, the boxed drawText() truncates overlong lines with "...". */ + bool ellipsis = false; + TextAlign align = TextAlign::Left; + TextVerticalAlign verticalAlign = TextVerticalAlign::Top; + TextWrap wrap = TextWrap::None; +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Include/gfx/Gfx.h b/Libraries/Gfx/Include/gfx/Gfx.h new file mode 100644 index 0000000..f6ac9e3 --- /dev/null +++ b/Libraries/Gfx/Include/gfx/Gfx.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +/** + * gfx - direct-to-display rendering for Tactility apps. + * + * Draw into a gfx::Surface inside a gfx::Target::render() callback; the target decides whether that + * is a hardware frame buffer flip, a dirty-region update, or a band-by-band render. The same app code + * then works on MIPI DSI, RGB, SPI and e-paper panels, with or without PSRAM. + * + * A gfx::Surface can also wrap any PixelBuffer directly, which makes the drawing primitives and + * text usable off-screen (sprites, canvases, or panels driven outside Tactility's display API). + */ + +#include "Canvas.h" +#include "DirtyRegions.h" +#include "DisplaySink.h" +#include "Font.h" +#include "Rect.h" +#include "Sink.h" +#include "Surface.h" +#include "Target.h" +#include "Text.h" diff --git a/Libraries/Gfx/Include/gfx/Rect.h b/Libraries/Gfx/Include/gfx/Rect.h new file mode 100644 index 0000000..54bfec6 --- /dev/null +++ b/Libraries/Gfx/Include/gfx/Rect.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +namespace gfx { + +/** A pixel coordinate. */ +struct Point { + int x = 0; + int y = 0; +}; + +/** A half-open pixel rectangle in world coordinates: x0/y0 inclusive, x1/y1 exclusive. */ +struct Rect { + int x0 = 0; + int y0 = 0; + int x1 = 0; + int y1 = 0; + + int width() const { return x1 - x0; } + int height() const { return y1 - y0; } + bool empty() const { return x0 >= x1 || y0 >= y1; } + + bool contains(int x, int y) const { return x >= x0 && x < x1 && y >= y0 && y < y1; } + + /** The overlapping part, or an empty rectangle when they don't overlap. */ + Rect intersect(const Rect& other) const { + const int nx0 = x0 > other.x0 ? x0 : other.x0; + const int ny0 = y0 > other.y0 ? y0 : other.y0; + const int nx1 = x1 < other.x1 ? x1 : other.x1; + const int ny1 = y1 < other.y1 ? y1 : other.y1; + if (nx0 >= nx1 || ny0 >= ny1) { + return {}; + } + return { nx0, ny0, nx1, ny1 }; + } + + /** The smallest rectangle covering both. An empty rectangle contributes nothing. */ + Rect unite(const Rect& other) const { + if (empty()) { + return other; + } + if (other.empty()) { + return *this; + } + return { + x0 < other.x0 ? x0 : other.x0, + y0 < other.y0 ? y0 : other.y0, + x1 > other.x1 ? x1 : other.x1, + y1 > other.y1 ? y1 : other.y1, + }; + } + + Rect offset(int dx, int dy) const { return { x0 + dx, y0 + dy, x1 + dx, y1 + dy }; } + + bool intersects(const Rect& other) const { return !intersect(other).empty(); } +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Include/gfx/Sink.h b/Libraries/Gfx/Include/gfx/Sink.h new file mode 100644 index 0000000..14af523 --- /dev/null +++ b/Libraries/Gfx/Include/gfx/Sink.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "DirtyRegions.h" +#include "Surface.h" +#include "Target.h" + +#include + +#include + +namespace gfx { + +/** The frame properties a Sink is asked to accept. */ +struct SinkInfo { + int width = 0; + int height = 0; + enum DisplayColorFormat format = DISPLAY_COLOR_FORMAT_RGB565; +}; + +/** + * A presentation backend for SinkTarget. + * + * A Sink owns no pixels: SinkTarget keeps the full-frame PixelBuffer and hands the sink the + * rectangles that changed. present() may be called many times per frame. Returning false is + * allowed when the backend cannot accept that region; SinkTarget logs it and carries on. + * + * Implementations should treat `buffer` as read-only and use `pixel_buffer_get_row()` for stride. + */ +class Sink { +public: + virtual ~Sink() = default; + + /** Called once before the first frame. Return false when the backend is unavailable. */ + virtual bool begin(const SinkInfo& info) = 0; + + /** Presents one changed rectangle. `rect` is clamped to the buffer and is never empty. */ + virtual bool present(const struct PixelBuffer& buffer, const Rect& rect) = 0; + + /** Called once when the target is destroyed. */ + virtual void end() = 0; + + /** + * Optional frame-buffer placement. Return true and fill `out` when the backend needs a + * specific MemoryPolicy (e.g. a display driver that DMAs from the buffer). The default leaves + * the buffer in external RAM when possible. + */ + virtual bool frame_buffer_policy(MemoryPolicy& out) const { + (void)out; + return false; + } + + /** Optional full-quality refresh, for e-paper-like backends. */ + virtual error_t refresh() { return ERROR_NOT_SUPPORTED; } +}; + +/** + * A display-independent Target for a Sink. + * + * The target owns one full-frame PixelBuffer and presents changed rectangles to the sink. It is + * the generic path for I2C LCD units, headless bitmaps, or any backend that is not a Tactility + * display device. Use gfx::Target for displays: it adds frame-buffer flipping, banded rendering + * and hardware rotation/scaling. + * + * The render buffer persists between frames, so Prepare is accepted for API symmetry but has no + * effect. Callers that only draw changed pixels can leave them alone; callers that need a clean + * slate should clear first. + */ +class SinkTarget { +public: + /** Takes a reference-free handle on `sink`; the sink must outlive this target. */ + SinkTarget(Sink& sink, int width, int height, enum DisplayColorFormat format = DISPLAY_COLOR_FORMAT_RGB565); + + ~SinkTarget(); + + SinkTarget(const SinkTarget&) = delete; + SinkTarget& operator=(const SinkTarget&) = delete; + + bool valid() const { return begun_ && frame_buffer_ != nullptr; } + + const Info& info() const { return info_; } + + const Stats& stats() const { return stats_; } + + /** The full-frame buffer the render callback draws into. Never owned by the caller. */ + struct PixelBuffer* buffer() { return frame_buffer_; } + + const struct PixelBuffer* buffer() const { return frame_buffer_; } + + /** Present a whole frame and then visit the sink's optional maintenance pass. */ + error_t refresh() { return sink_.refresh(); } + + using RenderCallback = void (*)(Surface& surface, void* context); + + void render(RenderCallback callback, void* context, Prepare prepare = Prepare::Preserve); + + template + void render(Function&& function, Prepare prepare = Prepare::Preserve) { + using Fn = std::remove_reference_t; + Fn* fn = &function; + render([](Surface& surface, void* context) { + (*static_cast(context))(surface); + }, fn, prepare); + } + +private: + Sink& sink_; + struct PixelBuffer* frame_buffer_ = nullptr; + DirtyRegions dirty_; + Info info_ {}; + Stats stats_ {}; + bool begun_ = false; + bool first_frame_ = true; +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Include/gfx/Surface.h b/Libraries/Gfx/Include/gfx/Surface.h new file mode 100644 index 0000000..ac7259c --- /dev/null +++ b/Libraries/Gfx/Include/gfx/Surface.h @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "Font.h" +#include "Rect.h" + +#include +#include + +#include +#include + +namespace gfx { + +class DirtyRegions; + +/** + * The drawable slab a render callback receives, or an off-screen buffer an app draws into. + * + * Coordinates are world coordinates in the display's own pixel space: x in [0, width()) and y in + * [originY(), originY() + height()). In banded mode the surface covers one horizontal band only, so + * anything drawn outside it is clipped. A render callback can therefore draw the whole scene + * without knowing which render mode the target picked. + * + * Buffers start black. In banded mode the target clears each band before the callback runs. In the + * flip and full-frame modes the buffer persists between frames, so a callback has to clear a region + * it drew into before drawing it again. + * + * Colour arguments are RGB565. Textures are written in the display's native format. + * + * A Surface never owns its buffer. Construct one from an existing PixelBuffer to draw off-screen: + * + * PixelBuffer* buffer = pixel_buffer_create(DISPLAY_COLOR_FORMAT_RGB565, 128, 64); + * gfx::Surface surface(buffer); + * surface.fillRect(0, 0, 128, 64, 0xF800); + * pixel_buffer_free(buffer); + */ +class Surface { +public: + Surface(const Surface&) = delete; + Surface& operator=(const Surface&) = delete; + + /** Wraps a whole buffer. Drawing coordinates start at the buffer's top-left. */ + explicit Surface(struct PixelBuffer* buffer); + + /** Wraps a sub-rectangle of a larger buffer. Drawing coordinates start at the view's top-left. */ + Surface(struct PixelBuffer* buffer, const Rect& view); + + int width() const { return bounds_.width(); } + + /** World rows this surface covers: the band height in banded mode, the display height otherwise. */ + int height() const { return bounds_.height(); } + + /** World x of this surface's first column. */ + int originX() const { return bounds_.x0; } + + /** World y of this surface's first row. */ + int originY() const { return bounds_.y0; } + + /** The world rectangle this surface covers. */ + const Rect& bounds() const { return bounds_; } + + /** The current clip, always inside bounds(). */ + const Rect& clip() const { return clip_; } + + /** Restricts drawing to `clip`, intersected with bounds(). */ + void setClip(const Rect& clip); + + /** Restores the clip to the whole surface. */ + void resetClip() { clip_ = bounds_; } + + enum DisplayColorFormat format() const { return format_; } + + /** True when the surface was constructed with a usable buffer. */ + bool valid() const { return buffer_ != nullptr && data_ != nullptr && !bounds_.empty(); } + + /** The underlying buffer, for handing to pixel_buffer_* calls. Never owned by the surface. */ + struct PixelBuffer* buffer() const { return buffer_; } + + /** Fills the whole surface with one colour (black by default). */ + void clear(uint16_t rgb565 = 0x0000); + + void fillRect(int x, int y, int width, int height, uint16_t rgb565); + + void fillRect(const Rect& rect, uint16_t rgb565) { fillRect(rect.x0, rect.y0, rect.width(), rect.height(), rgb565); } + + /** Horizontal line, `length` pixels starting at (x, y). */ + void hline(int x, int y, int length, uint16_t rgb565); + + /** Vertical line, `length` pixels starting at (x, y). */ + void vline(int x, int y, int length, uint16_t rgb565); + + /** Arbitrary line (Bresenham). */ + void line(int x0, int y0, int x1, int y1, uint16_t rgb565); + + /** Rectangle outline; `width` and `height` are the outer size. */ + void drawRect(int x, int y, int width, int height, uint16_t rgb565); + + void drawRect(const Rect& rect, uint16_t rgb565) { drawRect(rect.x0, rect.y0, rect.width(), rect.height(), rgb565); } + + /** Filled / outline circle (midpoint algorithm). */ + void fillCircle(int cx, int cy, int radius, uint16_t rgb565); + + void drawCircle(int cx, int cy, int radius, uint16_t rgb565); + + /** Filled / outline axis-aligned ellipse. */ + void fillEllipse(int cx, int cy, int radiusX, int radiusY, uint16_t rgb565); + + void drawEllipse(int cx, int cy, int radiusX, int radiusY, uint16_t rgb565); + + /** Rounded rectangle, filled / outline. `radius` is clamped to half the shorter side. */ + void fillRoundRect(int x, int y, int width, int height, int radius, uint16_t rgb565); + + void drawRoundRect(int x, int y, int width, int height, int radius, uint16_t rgb565); + + /** Triangle, filled / outline. */ + void fillTriangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t rgb565); + + void drawTriangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t rgb565); + + /** + * Filled / outline arc or annulus. Angles are in degrees: 0 is right, 90 is down, 180 is left, + * 270 is up. `outerRadius` and `innerRadius` describe the ring; pass 0 for a solid wedge. + */ + void fillArc(int cx, int cy, int outerRadius, int innerRadius, float startDegrees, float endDegrees, uint16_t rgb565); + + void drawArc(int cx, int cy, int outerRadius, int innerRadius, float startDegrees, float endDegrees, uint16_t rgb565); + + /** Fills a rectangle with a linear RGB565 gradient. */ + void gradient(const Rect& rect, uint16_t from, uint16_t to, bool horizontal = false); + + // ----------------------------------------------------------------------- + // Images and blending + // ----------------------------------------------------------------------- + + /** + * Copies a region from another buffer, converting formats as needed. Source and destination + * rectangles are clipped to the source buffer and this surface's clip. The source buffer must + * not overlap this surface. + */ + void blit(const struct PixelBuffer& source, int sourceX, int sourceY, int width, int height, int destX, int destY, enum PixelBufferConversion conversion = PIXEL_BUFFER_CONVERSION_EXACT_BLACK); + + /** + * Like blit(), but skips pixels equal to transparentRgb565 (a colour key). The source buffer + * must not overlap this surface. + */ + void blitTransparent(const struct PixelBuffer& source, int sourceX, int sourceY, int width, int height, int destX, int destY, uint16_t transparentRgb565, enum PixelBufferConversion conversion = PIXEL_BUFFER_CONVERSION_EXACT_BLACK); + + /** + * Draws a 1bpp icon mask in one colour. `maskStride` is bytes per mask row (0 means the tight + * default). `msbFirst` selects bit 7 (true) or bit 0 (false) as the leftmost pixel. + */ + void drawMask(const uint8_t* mask, int maskStride, int width, int height, int x, int y, uint16_t rgb565, bool msbFirst = true); + + /** + * Draws an 8bpp alpha mask, blending `rgb565` into the destination. `alphaStride` is bytes per + * mask row (0 means `width`). + */ + void drawAlphaMask(const uint8_t* alpha, int alphaStride, int width, int height, int x, int y, uint16_t rgb565); + + /** Blends one pixel over the existing destination colour. alpha is 0..255. */ + void blendPixel(int x, int y, uint16_t rgb565, uint8_t alpha); + + // ----------------------------------------------------------------------- + // Polygons + // ----------------------------------------------------------------------- + + /** Polygon outline. */ + void drawPolygon(const Point* points, int count, uint16_t rgb565); + + /** Even-odd polygon fill. At most MAX_POLYGON_POINTS vertices are supported. */ + void fillPolygon(const Point* points, int count, uint16_t rgb565); + + static constexpr int MAX_POLYGON_POINTS = 32; + + void pixel(int x, int y, uint16_t rgb565); + + /** + * Marks a world-space region as changed, for targets that present per region. Primitive calls + * already do this; call it directly after writing through a raw pointer. + */ + void markDirty(int x, int y, int width, int height); + + void markDirty(const Rect& rect) { markDirty(rect.x0, rect.y0, rect.width(), rect.height()); } + + // ----------------------------------------------------------------------- + // Text + // ----------------------------------------------------------------------- + + /** Draws transparent text: only glyph pixels are written. */ + void drawText(int x, int y, const char* text, const Font& font, uint16_t foreground); + + /** Draws text with an opaque background covering each glyph cell. */ + void drawText(int x, int y, const char* text, const Font& font, uint16_t foreground, uint16_t background); + + /** Draws text with an opaque background at an integer scale. */ + void drawText(int x, int y, const char* text, const Font& font, uint16_t foreground, uint16_t background, uint8_t scale); + + /** + * Draws text using a style. For Left/Top alignment `x`/`y` are the top-left of the block; for + * Center/Right and Middle/Bottom they are the anchor the block is aligned against. + */ + void drawText(int x, int y, const char* text, const TextStyle& style); + + /** + * Draws text inside a box, honouring the style's alignment and wrapping. Text that does not fit + * vertically is clipped. + */ + void drawText(const Rect& box, const char* text, const TextStyle& style); + +private: + friend class Target; + friend class SinkTarget; + + /** Internal: a buffer whose (0,0) maps to world (0, originY). Used for bands. */ + Surface(struct PixelBuffer* buffer, int originY, int rows, DirtyRegions* dirty); + + void initialise(struct PixelBuffer* buffer, const Rect& bounds, uint8_t* data, DirtyRegions* dirty); + + void writePixel(int x, int localY, uint16_t rgb565); + + /** Write with an explicit mono/grayscale conversion policy. */ + void writePixel(int x, int localY, uint16_t rgb565, enum PixelBufferConversion conversion); + + /** Reads a pixel back as RGB565. x/localY are surface-local coordinates. */ + uint16_t readPixel(int x, int localY) const; + + /** Blends into a pixel without touching the dirty set. World coordinates. */ + void blendPixelRaw(int x, int y, uint16_t rgb565, uint8_t alpha); + + bool isFastRgb565() const; + + uint16_t toNative565(uint16_t rgb565) const; + + void markDirtyRect(int x0, int y0, int x1, int y1); + + /** Clipped write with no dirty marking. */ + void plotRaw(int x, int y, uint16_t rgb565); + + /** Clipped horizontal span with no dirty marking; x1 is exclusive. */ + void fillSpanRaw(int x0, int x1, int y, uint16_t rgb565); + + /** Clipped rectangle with no dirty marking. */ + void fillRectRaw(const Rect& rect, uint16_t rgb565); + + /** Clipped line with no dirty marking. */ + void lineRaw(int x0, int y0, int x1, int y1, uint16_t rgb565); + + /** Draws one glyph without touching the dirty set. */ + void drawGlyph(int x, int y, char c, const Font& font, uint16_t foreground, uint16_t background, bool opaque, uint8_t scale); + + /** Draws one line of text without touching the dirty set. */ + void drawTextLine(int x, int y, const char* begin, const char* end, const TextStyle& style); + + struct PixelBuffer* buffer_ = nullptr; + uint8_t* data_ = nullptr; + std::size_t stride_ = 0; + Rect bounds_ {}; + Rect clip_ {}; + enum DisplayColorFormat format_ = DISPLAY_COLOR_FORMAT_RGB565; + DirtyRegions* dirty_ = nullptr; + /** Bit offset into the first byte of a monochrome sub-view. Zero for every other case. */ + int x_offset_bits_ = 0; +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Include/gfx/Target.h b/Libraries/Gfx/Include/gfx/Target.h new file mode 100644 index 0000000..689f6f4 --- /dev/null +++ b/Libraries/Gfx/Include/gfx/Target.h @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "DirtyRegions.h" +#include "Surface.h" + +#include + +namespace gfx { + +/** How the render buffer is prepared for a frame. */ +enum class Prepare { + /** + * Copy the previously displayed frame into the render buffer first, so the callback only has to + * redraw the regions it changes. Safe default, but costs a copy of the changed regions. + */ + Preserve, + /** + * Don't copy anything: the callback guarantees it overwrites every pixel that differs between + * frames, including content left over from earlier frames. Lets an app that draws thin + * primitives erase them by redrawing them in the background colour instead of clearing + * bounding boxes, which is far cheaper. + */ + Overwrite, +}; + +enum class RenderMode { + /** Construction failed; the target can't render. */ + None, + /** Two hardware frame buffers: the frame is drawn into the back one and presented by flipping. */ + Flip, + /** One full-frame buffer; only the regions that changed are sent to the display. */ + FullFrame, + /** A small band buffer, rendered and presented band by band. Uses the least memory. */ + Banded, + /** A logical frame that the display's scaler/rotator maps onto the panel (see set_transform()). */ + Transform, +}; + +/** A logical frame size plus the rotation applied when putting it on the panel. */ +struct Transform { + /** Size the render callback draws into. */ + int width = 0; + int height = 0; + /** Degrees clockwise to rotate when presenting: 0, 90, 180 or 270. */ + int rotation = 0; +}; + +struct Info { + int width = 0; + int height = 0; + enum DisplayColorFormat format = DISPLAY_COLOR_FORMAT_RGB565; + RenderMode mode = RenderMode::None; + + /** + * Size the render callback draws into: the native size unless set_transform() succeeded, in + * which case it is the logical (possibly rotated/scaled) size. + */ + int frameWidth = 0; + int frameHeight = 0; + + /** World rows per band for RenderMode::Banded, the display height otherwise. */ + int bandHeight = 0; + + /** The panel is slow to refresh (e-paper): animating it makes no sense. */ + bool slowRefresh = false; + + /** The panel needs a whole frame per update, so per-region presenting is not possible. */ + bool fullFrameRequired = false; + + /** Frames are rendered into the panel's own frame buffer(s), not into a scratch buffer. */ + bool hardwareFrameBuffer = false; +}; + +/** What the last rendered frame actually cost. Useful for tuning on real hardware. */ +struct Stats { + /** Rectangles the frame touched (1 for a whole-frame present). */ + int dirtyRects = 0; + /** Pixels covered by those rectangles. */ + int dirtyPixels = 0; + /** Microseconds per phase of the last frame (0 on platforms without a timer). */ + int copyUs = 0; + int drawUs = 0; + int syncUs = 0; + int presentUs = 0; + int totalUs = 0; +}; + +/** + * A display configured for direct rendering, outside LVGL. + * + * The target picks how to get pixels onto the panel: flip between two hardware frame buffers (MIPI + * DSI, RGB panels), present dirty regions from one full-frame buffer (panels with PSRAM to spare), + * or render band by band from a small buffer (panels with little RAM). Callers only see a Surface. + */ +class Target { +public: + /** Takes a reference on @a display and keeps it until destruction. */ + explicit Target(struct Device* display); + + ~Target(); + + Target(const Target&) = delete; + Target& operator=(const Target&) = delete; + + bool valid() const { return info_.mode != RenderMode::None; } + + /** + * Frees all render buffers and releases the PPA client. The display keeps its last presented + * frame. Intended for apps that use the primary display briefly and then need the memory for + * another target; further render() calls do nothing. + */ + void release(); + + const Info& info() const { return info_; } + + const Stats& stats() const { return stats_; } + + /** + * Runs the display's own refresh/maintenance pass, e.g. an e-paper panel's full-quality redraw + * that resolves grayscale or clears ghosting. Presenting does a fast partial update, so apps + * that draw static content on such a panel should call this once afterwards. + * @retval ERROR_NOT_SUPPORTED when the display has no refresh pass + */ + error_t refresh(); + + /** + * Draws into a logical frame and lets the display's scaler/rotator put it on the panel. Use it + * to work in landscape coordinates on a portrait-native panel, or to render at a lower + * resolution and let the hardware scale up. + * @retval true subsequent frames use the transform; info().frameWidth/frameHeight describe the + * surface the callback gets + * @retval false unsupported (no hardware scaler/rotator, or a display format it can't handle), + * in which case the target keeps rendering at the native size and rotation + */ + bool set_transform(const Transform& transform); + + /** Renders one frame. In banded mode the callback runs once per band with that band's surface. */ + using RenderCallback = void (*)(Surface& surface, void* context); + + void render(RenderCallback callback, void* context, Prepare prepare = Prepare::Preserve); + + template + void render(Function&& function, Prepare prepare = Prepare::Preserve) { + using Fn = std::remove_reference_t; + Fn* fn = &function; + render([](Surface& surface, void* context) { + (*static_cast(context))(surface); + }, fn, prepare); + } + +private: + bool setup(); + + bool setupFlip(); + + bool setupFullFrame(bool required); + + bool setupBanded(); + + Rect clampRect(const Rect& rect) const; + + void presentDirty(struct PixelBuffer* buffer); + + void copyRect(struct PixelBuffer* dst, struct PixelBuffer* src, const Rect& rect); + + void updateStats(bool whole_frame); + + void presentTransformed(struct PixelBuffer* output); + + struct Device* display_ = nullptr; + Info info_ {}; + Stats stats_ {}; + struct PixelBuffer* flip_buffers_[2] = { nullptr, nullptr }; + struct PixelBuffer* frame_buffer_ = nullptr; + struct PixelBuffer* band_buffer_ = nullptr; + // set_transform(): logical frame, its scaler client and the native-size output when there are + // no hardware frame buffers to write into + struct PixelBuffer* source_buffer_ = nullptr; + struct PixelBuffer* output_buffer_ = nullptr; + void* ppa_client_ = nullptr; + Transform transform_ {}; + bool using_hw_output_ = false; + uint8_t* staging_ = nullptr; + std::size_t staging_capacity_ = 0; + int back_index_ = 0; + bool present_full_ = false; + DirtyRegions dirty_; + // The regions the previous frame touched: the only ones that differ between the two flip + // buffers, so only those have to be copied forward + DirtyRegions previous_dirty_; + bool flip_has_previous_ = false; + bool first_frame_ = true; +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Include/gfx/Text.h b/Libraries/Gfx/Include/gfx/Text.h new file mode 100644 index 0000000..bf4489a --- /dev/null +++ b/Libraries/Gfx/Include/gfx/Text.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "Font.h" + +namespace gfx { + +/** + * Width in pixels of the widest line in `text`, at the given integer scale. Newlines split lines; + * the value includes the advance after the last glyph, so it is suitable for layout. + */ +int measureText(const char* text, const Font& font, uint8_t scale = 1, uint8_t tabSize = 4); + +/** Height in pixels of `text` including line gaps, at the given integer scale. */ +int measureTextHeight(const char* text, const Font& font, uint8_t scale = 1); + +/** Height of one line, including the gap below it. */ +inline int lineHeight(const Font& font, uint8_t scale = 1) { return font.lineHeight * (scale == 0 ? 1 : scale); } + +} // namespace gfx diff --git a/Libraries/Gfx/LICENSE.md b/Libraries/Gfx/LICENSE.md new file mode 100644 index 0000000..f5f4b8b --- /dev/null +++ b/Libraries/Gfx/LICENSE.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Libraries/Gfx/Licenses/Terminus-OFL.txt b/Libraries/Gfx/Licenses/Terminus-OFL.txt new file mode 100644 index 0000000..30a5496 --- /dev/null +++ b/Libraries/Gfx/Licenses/Terminus-OFL.txt @@ -0,0 +1,99 @@ +[for the terminus font included here] + +Copyright (c) 2010 Dimitar Toshkov Zhekov, +with Reserved Font Name "Terminus Font". + +Copyright (c) 2011-2023 Tilman Blumenbach, +with Reserved Font Name "Terminus (TTF)". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Libraries/Gfx/README.md b/Libraries/Gfx/README.md new file mode 100644 index 0000000..2a151e7 --- /dev/null +++ b/Libraries/Gfx/README.md @@ -0,0 +1,329 @@ +# Gfx + +Direct-to-display rendering for Tactility apps, outside LVGL. + +`gfx::Target` takes a display device and picks the cheapest way to get pixels onto the panel: + +| Panel | Mode | How | +|---|---|---| +| Two hardware frame buffers (MIPI DSI, RGB) | `Flip` | draw into the back buffer, present by flipping | +| One full-frame buffer fits | `FullFrame` | draw once, send only the dirty regions | +| Little RAM (ESP32, no PSRAM) | `Banded` | render and present one band at a time | +| Mono / `REQUIRES_FULL_FRAME` (e-paper) | `FullFrame` | always send whole frames | + +Apps draw into a `gfx::Surface` and never need to know which mode was picked: + +```cpp +#include + +gfx::Target target(display_device); +if (!target.valid()) { /* unsupported display */ } + +while (running) { + advanceState(); + target.render([this](gfx::Surface& surface) { + surface.fillRect(x, y, w, h, 0x0000); // clear what moves + surface.line(x0, y0, x1, y1, 0xF800); // draw + }); +} +``` + +See [DESIGN.md](DESIGN.md) for the architecture and the roadmap (images, canvases, extra fonts, +custom presentation backends). + +## Coordinates and clipping + +Surface coordinates are world coordinates in the display's pixel space: `x` in `[0, width())`, +`y` in `[originY(), originY() + height())`. In banded mode the callback runs once per band with a +surface covering only that band, so drawing outside it is clipped for free. The callback must not +mutate app state: it can run more than once per frame. + +A surface also has a clip rectangle, defaulting to its full bounds. Restrict drawing to a region +without changing the render mode: + +```cpp +surface.setClip({ 10, 10, 100, 60 }); +surface.fillRect(0, 0, 200, 200, 0x001F); // only the part inside the clip is written +surface.resetClip(); +``` + +## Primitives + +```cpp +surface.clear(uint16_t rgb565 = 0x0000); +surface.pixel(x, y, rgb565); + +surface.hline(x, y, length, rgb565); +surface.vline(x, y, length, rgb565); +surface.line(x0, y0, x1, y1, rgb565); +surface.fillRect(x, y, width, height, rgb565); +surface.drawRect(x, y, width, height, rgb565); + +surface.fillCircle(cx, cy, radius, rgb565); +surface.drawCircle(cx, cy, radius, rgb565); +surface.fillEllipse(cx, cy, radiusX, radiusY, rgb565); +surface.drawEllipse(cx, cy, radiusX, radiusY, rgb565); +surface.fillRoundRect(x, y, width, height, radius, rgb565); +surface.drawRoundRect(x, y, width, height, radius, rgb565); +surface.fillTriangle(x0, y0, x1, y1, x2, y2, rgb565); +surface.drawTriangle(x0, y0, x1, y1, x2, y2, rgb565); + +// Angles: 0 = right, 90 = down, 180 = left, 270 = up. +surface.fillArc(cx, cy, outerRadius, innerRadius, startDegrees, endDegrees, rgb565); +surface.drawArc(cx, cy, outerRadius, innerRadius, startDegrees, endDegrees, rgb565); + +surface.gradient({ x, y, x + w, y + h }, fromRgb565, toRgb565); +``` + +All colours are RGB565. `Surface` converts to the panel's native format (including RGB888, +GRAYSCALE8 and MONOCHROME) as it writes. + +## Text and fonts + +Text is drawn from bitmap fonts. Built-in fonts: + +| Font | Size | Notes | +|---|---|---| +| `gfx::FONT_5X7` | 5x7, 6px advance | Classic terminal font | +| `gfx::FONT_5X7_PROPORTIONAL` | 5x7, per-glyph advance | Same glyphs, proportional spacing | +| `gfx::FONT_8X8` | 8x8, 8px advance | Public-domain VGA-style font | +| `gfx::FONT_8X16` | 8x16, 8px advance | Terminus body font, Latin-1 (OFL-1.1; see `Licenses/Terminus-OFL.txt`) | + +`FONT_8X16` at `scale = 2` gives a 16x32 cell for large display text. + +```cpp +// Transparent: only glyph pixels are written. +surface.drawText(8, 8, "Hello", gfx::FONT_8X8, 0xFFFF); + +// Opaque: each glyph cell gets a background, including the gap after the glyph. +surface.drawText(8, 32, "Hello", gfx::FONT_5X7, 0xFFFF, 0x0000, 2); +``` + +For layout, use `TextStyle`: + +```cpp +gfx::TextStyle style; +style.font = &gfx::FONT_5X7; +style.foreground = 0xFFFF; +style.background = 0x0010; +style.opaque = true; +style.scale = 2; +style.align = gfx::TextAlign::Center; +style.verticalAlign = gfx::TextVerticalAlign::Middle; +style.wrap = gfx::TextWrap::Word; + +// x/y are the anchor for Center/Right and Middle/Bottom. +surface.drawText(160, 120, "Centered", style); + +// Or lay out inside a box, clipped to it. +surface.drawText({ 8, 8, 312, 60 }, "A longer line that wraps at spaces", style); +``` + +Measurement: + +```cpp +int width = gfx::measureText("Hello", gfx::FONT_8X8); +int height = gfx::measureTextHeight("Hello", gfx::FONT_8X8); + +// Tabs use style.tabSize cells; measureText() defaults to 4. +int tabbed = gfx::measureText("a\tb", gfx::FONT_5X7, 1, 2); +``` + +`TextStyle` also supports proportional fonts, configurable tab width, and ellipsis: + +```cpp +gfx::TextStyle style; +style.font = &gfx::FONT_5X7_PROPORTIONAL; +style.foreground = 0xFFFF; +style.tabSize = 2; +style.ellipsis = true; + +// A line too wide for the box is truncated with "...". +surface.drawText({ 8, 8, 120, 24 }, "A very long line", style); +``` + +`FONT_5X7_PROPORTIONAL` uses per-glyph widths and advances while sharing the 5x7 glyph +data. `Font` can carry optional `glyphWidths` and `advances` tables for app-supplied +proportional fonts. + +`TextStyle::scale` is an integer magnification. Small text should use a native font; large display +text can be scaled. A `Font` is plain data (`glyphWidth`, `glyphHeight`, `advance`, `lineHeight`, +layout and a pointer to 1bpp glyphs), so apps can supply their own font without changing the +library. + +The text pipeline is byte-oriented: 5x7 and 8x8 cover ASCII, and `FONT_8X16` also has Latin-1 +glyphs. UTF-8 decoding is not implemented yet, so multi-byte UTF-8 sequences render as individual +bytes (or `?` when the selected font has no glyph for that byte). + +## Images and masks + +`Surface::blit()` copies a region from another `PixelBuffer`, clipping against both the source +buffer and the destination clip, and converting formats as needed: + +```cpp +PixelBuffer* source = pixel_buffer_create(DISPLAY_COLOR_FORMAT_RGB565, 32, 32); +gfx::Surface sprite(source); +sprite.fillCircle(16, 16, 12, 0xF800); + +surface.blit(*source, 0, 0, 32, 32, x, y); + +// Treat black as transparent (colour key). +surface.blitTransparent(*source, 0, 0, 32, 32, x, y, 0x0000); +``` + +Same-format blits use per-row `memcpy`; transparent blits over a fast RGB565 format compare native +pixels directly. Cross-format blits go through RGB565 conversion per pixel. + +1bpp and 8bpp masks cover icons and soft edges: + +```cpp +// 1bpp mask, MSB first, one byte per row. +surface.drawMask(mask, maskStride, width, height, x, y, 0xFFFF, true); + +// 8bpp alpha mask: 0 = transparent, 255 = solid. +surface.drawAlphaMask(alpha, alphaStride, width, height, x, y, 0xFFFF); + +// Blend a single pixel. +surface.blendPixel(x, y, 0xF800, 128); +``` + +The Material Symbols data already used by some apps is 2bpp LVGL font data rather than a 1bpp +mask, so a 2bpp glyph decoder is a future addition; the 1bpp/8bpp paths are independent of it. + +## Polygons + +```cpp +const gfx::Point star[] = { + { 50, 10 }, { 61, 40 }, { 95, 40 }, { 68, 58 }, { 80, 90 }, + { 50, 70 }, { 20, 90 }, { 32, 58 }, { 5, 40 }, { 39, 40 }, +}; + +surface.fillPolygon(star, 10, 0xF800); // even-odd scanline fill +surface.drawPolygon(star, 10, 0xFFFF); // outline +``` + +`fillPolygon()` supports up to `gfx::Surface::MAX_POLYGON_POINTS` (32) vertices and marks its +bounding box dirty once, like the other compound primitives. + +## Off-screen surfaces + +A `Surface` does not need a `Target`. Wrap any `PixelBuffer` directly to compose sprites, render +into an app-owned canvas, or drive a panel that is not a Tactility display device: + +```cpp +#include +#include + +PixelBuffer* buffer = pixel_buffer_create(DISPLAY_COLOR_FORMAT_RGB565, 128, 64); +gfx::Surface surface(buffer); +surface.clear(0x0841); +surface.drawText(4, 4, "canvas", gfx::FONT_5X7, 0xFFFF); +// ... hand pixel_buffer_get_data(buffer) to whatever presents it ... +pixel_buffer_free(buffer); +``` + +Sub-views make it easy to draw into part of a larger buffer: + +```cpp +gfx::Surface view(buffer, { 16, 16, 48, 32 }); // x0, y0, x1, y1 +view.fillCircle(16, 8, 8, 0xF800); // coordinates start at the view's top-left +``` + +`gfx::Canvas` owns the buffer for you and keeps the `Surface` in sync: + +```cpp +gfx::Canvas sprite(32, 32, DISPLAY_COLOR_FORMAT_RGB565); +if (sprite.valid()) { + sprite.clear(0xF81F); // magenta colour key + sprite.surface().fillCircle(16, 16, 12, 0x07FF); + sprite.surface().drawText(4, 12, "OK", gfx::FONT_5X7, 0xFFFF); +} + +target.render([&](gfx::Surface& s) { + s.blitTransparent(*sprite.buffer(), 0, 0, 32, 32, x, y, 0xF81F); +}); +``` + +## Presentation sinks + +`gfx::Target` is the optimized display path. For an I2C LCD, a headless bitmap, or any other +backend, implement `gfx::Sink` and let `gfx::SinkTarget` own the frame buffer and dirty tracking: + +```cpp +class MySink : public gfx::Sink { +public: + bool begin(const gfx::SinkInfo& info) override { /* open the backend */ return true; } + bool present(const PixelBuffer& buffer, const gfx::Rect& rect) override { + // send buffer's rows [rect.y0, rect.y1), columns [rect.x0, rect.x1) + return true; + } + void end() override { /* close the backend */ } +}; + +MySink sink; +gfx::SinkTarget target(sink, 128, 64, DISPLAY_COLOR_FORMAT_RGB565); +if (target.valid()) { + target.render([](gfx::Surface& surface) { + surface.clear(0x0000); + surface.drawText(4, 4, "sink", gfx::FONT_8X16, 0xFFFF); + }); +} +``` + +- `SinkTarget` owns one full-frame `PixelBuffer`, records dirty regions, and calls + `Sink::present()` for each changed rect. The buffer persists between frames. +- Override `frame_buffer_policy()` when the backend needs DMA-capable memory; otherwise the frame + buffer prefers external RAM. +- `gfx::DisplaySink` adapts a Tactility display device using `display_draw_bitmap()` (whole rows). + `gfx::Target` remains the better choice for displays because it adds flip, banded rendering and + hardware transform support. +- When memory is tight, `Target::release()` frees the primary render buffers; the panel keeps its + last presented frame. The UnitLCD demo instead allocates the LCD frame first and creates the + optional primary status target afterwards. +- `UnitLcdSink` lives in `Libraries/UnitLcdGfx` and presents to the M5Stack LCD Unit without + changing `M5UnitModules`. It takes an origin, so it can own a sub-frame: draw the static chrome + with the unit's own primitives and let the sink drive only the part that changes. A smaller frame + buffer is often the difference between working and not working on no-PSRAM boards. + +## What the callback must guarantee + +Buffers start black. In banded mode the target clears each band before calling back. In flip and +full-frame mode the buffer persists between frames, so **clear a region before drawing into it +again** — the target only knows about the pixels you report. + +Primitives mark their own dirty regions. If you write through a raw pointer, call `markDirty()`. + +## Memory + +- Banded mode uses a 32 KB budget by default and keeps bands in internal DMA-capable memory, + because SPI panel drivers DMA straight from the buffer and ESP32-class chips can't DMA from PSRAM. +- Full-frame mode requires the frame to fit with 2x headroom, and requires DMA-capable memory + unless the display reports `PREFER_EXTERNAL_RAM` (meaning its driver copies the data). + +## Presenting + +`DisplayApi::draw_bitmap()` has no stride parameter: the driver assumes the pixels for the rectangle +it is given are tightly packed. Dirty rectangles are therefore copied through a small staging buffer +(a few tens of KB, internal DMA memory) before being presented, so a driver only has to drive the +rectangle itself — which matters a lot on e-paper, where the panel is refreshed per region. When no +staging buffer could be allocated, presenting falls back to whole rows. + +Banded mode sends whole bands, which are only a few rows tall. + +No widgets, no event loop. This is surfaces, primitives, text and presenting. `pixel_buffer_*` from the +`graphics-module` provides the buffers, format conversion and cache writeback; `gfx` adds the +drawing and the present policy on top. + +## Example app + +[`Apps/GfxBenchmark`](../../Apps/GfxBenchmark) is a PDQ-style primitive benchmark built on this +library. It runs the same phases as the M5UnitTest LCD GFX test through `gfx::Target`, adapts the +workload to the display, and draws its results table with the new font/text API. It is a good +end-to-end workout for new primitives and render modes. + +## Licenses + +The library code follows [LICENSE.md](LICENSE.md). The 8x16 Terminus font data in +`Source/FontTerminus16.cpp` is licensed under the SIL Open Font License 1.1; see +[Licenses/Terminus-OFL.txt](Licenses/Terminus-OFL.txt). The 5x7 and 8x8 fonts are public-domain +bitmap fonts. diff --git a/Libraries/Gfx/Source/Canvas.cpp b/Libraries/Gfx/Source/Canvas.cpp new file mode 100644 index 0000000..1aaee94 --- /dev/null +++ b/Libraries/Gfx/Source/Canvas.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +namespace gfx { + +Canvas::Canvas(int width, int height, enum DisplayColorFormat format, const MemoryPolicy* policy) + : buffer_(policy != nullptr + ? pixel_buffer_create_with_policy(format, width, height, policy) + : pixel_buffer_create(format, width, height)), + surface_(buffer_) { +} + +Canvas::~Canvas() { + if (buffer_ != nullptr) { + pixel_buffer_free(buffer_); + } +} + +} // namespace gfx diff --git a/Libraries/Gfx/Source/ColorUtils.h b/Libraries/Gfx/Source/ColorUtils.h new file mode 100644 index 0000000..1f93eaa --- /dev/null +++ b/Libraries/Gfx/Source/ColorUtils.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include + +namespace gfx::detail { + +inline uint16_t byteswap16(uint16_t value) { + return static_cast((value >> 8) | (value << 8)); +} + +// Swapping the 5-bit R and B fields is its own inverse, so this also converts BGR565 -> RGB565. +inline uint16_t swapRedBlue(uint16_t value) { + const uint16_t r = (value >> 11) & 0x1F; + const uint16_t g = (value >> 5) & 0x3F; + const uint16_t b = value & 0x1F; + return static_cast((b << 11) | (g << 5) | r); +} + +inline void toRgb888(uint16_t value, uint8_t out[3]) { + const uint32_t r5 = (value >> 11) & 0x1F; + const uint32_t g6 = (value >> 5) & 0x3F; + const uint32_t b5 = value & 0x1F; + out[0] = static_cast((r5 << 3) | (r5 >> 2)); + out[1] = static_cast((g6 << 2) | (g6 >> 4)); + out[2] = static_cast((b5 << 3) | (b5 >> 2)); +} + +inline uint16_t rgb888To565(const uint8_t rgb[3]) { + return static_cast(((rgb[0] >> 3) << 11) | ((rgb[1] >> 2) << 5) | (rgb[2] >> 3)); +} + +inline uint8_t toLuma(uint16_t value) { + uint8_t rgb[3]; + toRgb888(value, rgb); + const uint32_t luma = static_cast(rgb[0]) * 299 + static_cast(rgb[1]) * 587 + static_cast(rgb[2]) * 114; + return static_cast(luma / 1000); +} + +inline uint8_t toAverage(uint16_t value) { + uint8_t rgb[3]; + toRgb888(value, rgb); + return static_cast((static_cast(rgb[0]) + rgb[1] + rgb[2]) / 3); +} + +inline uint8_t toGrayscale(uint16_t value, enum PixelBufferConversion conversion) { + return conversion == PIXEL_BUFFER_CONVERSION_AVERAGE ? toAverage(value) : toLuma(value); +} + +inline bool isInk(uint16_t value, enum PixelBufferConversion conversion) { + if (conversion == PIXEL_BUFFER_CONVERSION_EXACT_BLACK) { + return value != 0x0000; + } + return toGrayscale(value, conversion) >= 128; +} + +/** Encodes an RGB565 colour into a fast RGB565-family format's native 16-bit value. */ +inline uint16_t encode565(enum DisplayColorFormat format, uint16_t rgb565) { + switch (format) { + case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: + return byteswap16(rgb565); + case DISPLAY_COLOR_FORMAT_BGR565: + return swapRedBlue(rgb565); + case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: + return byteswap16(swapRedBlue(rgb565)); + case DISPLAY_COLOR_FORMAT_RGB565: + default: + return rgb565; + } +} + +/** Decodes a fast RGB565-family native value back to RGB565. */ +inline uint16_t decode565(enum DisplayColorFormat format, uint16_t native) { + switch (format) { + case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: + return byteswap16(native); + case DISPLAY_COLOR_FORMAT_BGR565: + return swapRedBlue(native); + case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: + return swapRedBlue(byteswap16(native)); + case DISPLAY_COLOR_FORMAT_RGB565: + default: + return native; + } +} + +inline uint16_t blend565(uint16_t destination, uint16_t source, uint8_t alpha) { + const int a = alpha; + const int dr = (destination >> 11) & 0x1F; + const int dg = (destination >> 5) & 0x3F; + const int db = destination & 0x1F; + const int sr = (source >> 11) & 0x1F; + const int sg = (source >> 5) & 0x3F; + const int sb = source & 0x1F; + const int r = dr + (sr - dr) * a / 255; + const int g = dg + (sg - dg) * a / 255; + const int b = db + (sb - db) * a / 255; + return static_cast((r << 11) | (g << 5) | b); +} + +} // namespace gfx::detail diff --git a/Libraries/Gfx/Source/DisplaySink.cpp b/Libraries/Gfx/Source/DisplaySink.cpp new file mode 100644 index 0000000..e608c23 --- /dev/null +++ b/Libraries/Gfx/Source/DisplaySink.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include + +namespace gfx { + +DisplaySink::DisplaySink(struct Device* display) : display_(display) { + if (display_ != nullptr && device_get(display_) != ERROR_NONE) { + display_ = nullptr; + } +} + +DisplaySink::~DisplaySink() { + if (display_ != nullptr) { + device_put(display_); + } +} + +bool DisplaySink::begin(const SinkInfo& info) { + if (display_ == nullptr || info.width <= 0 || info.height <= 0) { + return false; + } + if (info.format != display_get_color_format(display_)) { + return false; + } + if (info.width != display_get_resolution_x(display_) || info.height != display_get_resolution_y(display_)) { + return false; + } + info_ = info; + begun_ = true; + return true; +} + +bool DisplaySink::present(const struct PixelBuffer& buffer, const Rect& rect) { + if (!begun_) { + return false; + } + + const Rect clipped = rect.intersect(Rect { 0, 0, info_.width, info_.height }); + if (clipped.empty()) { + return true; + } + + auto* mutable_buffer = const_cast(&buffer); + // draw_bitmap() has no stride parameter, so present whole rows. This works for every format, + // including monochrome, and keeps a sub-rectangle from leaking into the next row. + pixel_buffer_msync(mutable_buffer, 0, clipped.y0, info_.width, clipped.height()); + return display_draw_bitmap( + display_, + 0, + clipped.y0, + info_.width, + clipped.y1, + pixel_buffer_get_row(mutable_buffer, clipped.y0) + ) == ERROR_NONE; +} + +void DisplaySink::end() { + begun_ = false; +} + +bool DisplaySink::frame_buffer_policy(MemoryPolicy& out) const { + if (display_ == nullptr) { + return false; + } + // If the driver copies out of the buffer, external RAM is fine. Otherwise the driver may DMA + // straight from it, which PSRAM can't do on ESP32-class chips. + if (display_has_capability(display_, DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM)) { + return false; + } + out = MemoryPolicy { MEMORY_CAPABILITY_DMA, MEMORY_CAPABILITY_EXTERNAL, 64 }; + return true; +} + +error_t DisplaySink::refresh() { + if (display_ == nullptr) { + return ERROR_INVALID_STATE; + } + return display_refresh(display_); +} + +} // namespace gfx diff --git a/Libraries/Gfx/Source/FontTerminus16.cpp b/Libraries/Gfx/Source/FontTerminus16.cpp new file mode 100644 index 0000000..d70462d --- /dev/null +++ b/Libraries/Gfx/Source/FontTerminus16.cpp @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: OFL-1.1 +// +// Terminus Font 8x16 bitmap data, Latin-1 range 0x20-0xFF. +// Copyright (c) 2010 Dimitar Toshkov Zhekov, with Reserved Font Name "Terminus Font". +// Copyright (c) 2011-2023 Tilman Blumenbach, with Reserved Font Name "Terminus (TTF)". +// SIL Open Font License 1.1; see Licenses/Terminus-OFL.txt. +// +// Originally generated for the BreezyBox vterm from Terminus TTF 4.49.3 with lv_font_conv. +// One byte per row, bit 7 is the leftmost pixel. The source range skips U+007F-U+009F; those +// slots are blank here so the font stays contiguous. + +#include + +namespace gfx { + +static const uint8_t FONT_8X16_DATA[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x24, 0x24, 0x7E, 0x24, 0x24, 0x7E, 0x24, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x10, 0x7C, 0x92, 0x90, 0x90, 0x7C, 0x12, 0x12, 0x92, 0x7C, 0x10, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x64, 0x94, 0x68, 0x08, 0x10, 0x10, 0x20, 0x2C, 0x52, 0x4C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x24, 0x24, 0x18, 0x30, 0x4A, 0x44, 0x44, 0x44, 0x3A, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x10, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x18, 0x7E, 0x18, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x7C, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x20, 0x20, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x46, 0x4A, 0x52, 0x62, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x18, 0x28, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x02, 0x1C, 0x02, 0x02, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x06, 0x0A, 0x12, 0x22, 0x42, 0x7E, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x7C, 0x02, 0x02, 0x02, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1C, 0x20, 0x40, 0x40, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7E, 0x02, 0x02, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x02, 0x02, 0x04, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0x08, 0x10, 0x20, 0x40, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x40, 0x20, 0x10, 0x08, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x04, 0x08, 0x08, 0x00, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7C, 0x82, 0x9E, 0xA2, 0xA2, 0xA2, 0xA6, 0x9A, 0x80, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7C, 0x42, 0x42, 0x42, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x40, 0x40, 0x40, 0x40, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x78, 0x44, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x44, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x78, 0x40, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x78, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x40, 0x40, 0x4E, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x42, 0x44, 0x48, 0x50, 0x60, 0x60, 0x50, 0x48, 0x44, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x82, 0xC6, 0xAA, 0x92, 0x92, 0x82, 0x82, 0x82, 0x82, 0x82, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x42, 0x42, 0x42, 0x62, 0x52, 0x4A, 0x46, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x7C, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x4A, 0x3C, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x7C, 0x50, 0x48, 0x44, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x40, 0x40, 0x3C, 0x02, 0x02, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFE, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x24, 0x24, 0x24, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x82, 0x82, 0x82, 0x82, 0x82, 0x92, 0x92, 0xAA, 0xC6, 0x82, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x42, 0x42, 0x24, 0x24, 0x18, 0x18, 0x24, 0x24, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x82, 0x82, 0x44, 0x44, 0x28, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7E, 0x02, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x38, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x40, 0x20, 0x20, 0x10, 0x10, 0x08, 0x08, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x38, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, + 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x02, 0x3E, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x40, 0x40, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x42, 0x40, 0x40, 0x40, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x02, 0x02, 0x3E, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x42, 0x42, 0x7E, 0x40, 0x40, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0E, 0x10, 0x10, 0x7C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x02, 0x02, 0x3C, 0x00, + 0x00, 0x00, 0x40, 0x40, 0x40, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x10, 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x44, 0x44, 0x38, 0x00, + 0x00, 0x00, 0x40, 0x40, 0x40, 0x42, 0x44, 0x48, 0x70, 0x48, 0x44, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7C, 0x40, 0x40, 0x40, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x02, 0x02, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E, 0x60, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x40, 0x40, 0x3C, 0x02, 0x02, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x10, 0x10, 0x7C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x24, 0x24, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x82, 0x92, 0x92, 0x92, 0x92, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x42, 0x24, 0x18, 0x24, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x02, 0x02, 0x3C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x04, 0x08, 0x10, 0x20, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0C, 0x10, 0x10, 0x10, 0x20, 0x10, 0x10, 0x10, 0x10, 0x0C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x30, 0x08, 0x08, 0x08, 0x04, 0x08, 0x08, 0x08, 0x08, 0x30, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x62, 0x92, 0x8C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x10, 0x7C, 0x92, 0x90, 0x90, 0x90, 0x92, 0x7C, 0x10, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x24, 0x20, 0x20, 0x78, 0x20, 0x20, 0x20, 0x22, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0x38, 0x44, 0x44, 0x44, 0x38, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x82, 0x82, 0x44, 0x28, 0x10, 0x7C, 0x10, 0x7C, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x38, 0x44, 0x40, 0x30, 0x48, 0x44, 0x44, 0x24, 0x18, 0x04, 0x44, 0x38, 0x00, 0x00, 0x00, + 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7E, 0x81, 0x99, 0xA5, 0xA1, 0xA5, 0x99, 0x81, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x38, 0x04, 0x3C, 0x44, 0x3C, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x24, 0x48, 0x90, 0x48, 0x24, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7E, 0x81, 0xB9, 0xA5, 0xB9, 0xA9, 0xA5, 0x81, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x24, 0x24, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x7C, 0x10, 0x10, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x24, 0x04, 0x08, 0x10, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x38, 0x04, 0x18, 0x04, 0x04, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x46, 0x7A, 0x40, 0x40, 0x40, 0x00, + 0x00, 0x00, 0x7E, 0x92, 0x92, 0x92, 0x92, 0x72, 0x12, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00, + 0x00, 0x10, 0x30, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x48, 0x24, 0x12, 0x24, 0x48, 0x90, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, 0x60, 0x20, 0x22, 0x24, 0x08, 0x10, 0x22, 0x46, 0x8A, 0x1E, 0x02, 0x02, 0x00, 0x00, + 0x00, 0x20, 0x60, 0x20, 0x22, 0x24, 0x08, 0x10, 0x20, 0x4C, 0x92, 0x04, 0x08, 0x1E, 0x00, 0x00, + 0x00, 0xE0, 0x10, 0x60, 0x12, 0xE4, 0x08, 0x10, 0x22, 0x46, 0x8A, 0x1E, 0x02, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x20, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x08, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x10, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x24, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x32, 0x4C, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x24, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x24, 0x18, 0x3C, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7E, 0x90, 0x90, 0x90, 0xFC, 0x90, 0x90, 0x90, 0x90, 0x9E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x42, 0x42, 0x40, 0x40, 0x40, 0x40, 0x42, 0x42, 0x3C, 0x10, 0x10, 0x20, 0x00, + 0x10, 0x08, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x78, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x10, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x78, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x24, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x78, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x24, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x78, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x10, 0x00, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x10, 0x00, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x24, 0x00, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x44, 0x00, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x78, 0x44, 0x42, 0x42, 0xF2, 0x42, 0x42, 0x42, 0x44, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x32, 0x4C, 0x00, 0x42, 0x42, 0x62, 0x52, 0x4A, 0x46, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x08, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x10, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x24, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x32, 0x4C, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x24, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x43, 0x42, 0x46, 0x4A, 0x52, 0x62, 0x42, 0xC2, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x08, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x10, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x24, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x24, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x10, 0x82, 0x82, 0x44, 0x44, 0x28, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x40, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x7C, 0x40, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x38, 0x44, 0x44, 0x48, 0x7C, 0x42, 0x42, 0x42, 0x62, 0x5C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x08, 0x00, 0x3C, 0x02, 0x3E, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x10, 0x00, 0x3C, 0x02, 0x3E, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x24, 0x00, 0x3C, 0x02, 0x3E, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x32, 0x4C, 0x00, 0x3C, 0x02, 0x3E, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x24, 0x00, 0x3C, 0x02, 0x3E, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x24, 0x18, 0x3C, 0x02, 0x3E, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x6C, 0x12, 0x72, 0x9E, 0x90, 0x90, 0x6C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x42, 0x40, 0x40, 0x40, 0x42, 0x3C, 0x10, 0x10, 0x20, 0x00, + 0x00, 0x00, 0x10, 0x08, 0x00, 0x3C, 0x42, 0x42, 0x7E, 0x40, 0x40, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x10, 0x00, 0x3C, 0x42, 0x42, 0x7E, 0x40, 0x40, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x24, 0x00, 0x3C, 0x42, 0x42, 0x7E, 0x40, 0x40, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x24, 0x00, 0x3C, 0x42, 0x42, 0x7E, 0x40, 0x40, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x10, 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x10, 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x30, 0x48, 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x48, 0x48, 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x28, 0x10, 0x28, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x32, 0x4C, 0x00, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x08, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x10, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x24, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x32, 0x4C, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x24, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x7C, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x3C, 0x46, 0x4A, 0x52, 0x62, 0x42, 0xBC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x08, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x10, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x24, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x24, 0x24, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x10, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x02, 0x02, 0x3C, 0x00, + 0x00, 0x00, 0x40, 0x40, 0x40, 0x7C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7C, 0x40, 0x40, 0x40, 0x00, + 0x00, 0x00, 0x24, 0x24, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3E, 0x02, 0x02, 0x3C, 0x00, +}; + +const Font FONT_8X16 = { + 8, + 16, + 8, + 16, + 0x20, + 0xFF, + FontLayout::Rows, + FONT_8X16_DATA, +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Source/Fonts.cpp b/Libraries/Gfx/Source/Fonts.cpp new file mode 100644 index 0000000..4f52844 --- /dev/null +++ b/Libraries/Gfx/Source/Fonts.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +namespace gfx { + +// Classic 5x7 terminal font, ASCII 32-126. One byte per column, bit 0 is the top row. +// This is the same glyph data the M5Stack LCD unit (UnitLcd) uses. +static const uint8_t FONT_5X7_DATA[][5] = { + { 0x00, 0x00, 0x00, 0x00, 0x00 }, // ' ' + { 0x00, 0x00, 0x5F, 0x00, 0x00 }, // '!' + { 0x00, 0x07, 0x00, 0x07, 0x00 }, // '"' + { 0x14, 0x7F, 0x14, 0x7F, 0x14 }, // '#' + { 0x24, 0x2A, 0x7F, 0x2A, 0x12 }, // '$' + { 0x23, 0x13, 0x08, 0x64, 0x62 }, // '%' + { 0x36, 0x49, 0x55, 0x22, 0x50 }, // '&' + { 0x00, 0x05, 0x03, 0x00, 0x00 }, // '\'' + { 0x00, 0x1C, 0x22, 0x41, 0x00 }, // '(' + { 0x00, 0x41, 0x22, 0x1C, 0x00 }, // ')' + { 0x08, 0x2A, 0x1C, 0x2A, 0x08 }, // '*' + { 0x08, 0x08, 0x3E, 0x08, 0x08 }, // '+' + { 0x00, 0x50, 0x30, 0x00, 0x00 }, // ',' + { 0x08, 0x08, 0x08, 0x08, 0x08 }, // '-' + { 0x00, 0x60, 0x60, 0x00, 0x00 }, // '.' + { 0x20, 0x10, 0x08, 0x04, 0x02 }, // '/' + { 0x3E, 0x51, 0x49, 0x45, 0x3E }, // '0' + { 0x00, 0x42, 0x7F, 0x40, 0x00 }, // '1' + { 0x42, 0x61, 0x51, 0x49, 0x46 }, // '2' + { 0x21, 0x41, 0x45, 0x4B, 0x31 }, // '3' + { 0x18, 0x14, 0x12, 0x7F, 0x10 }, // '4' + { 0x27, 0x45, 0x45, 0x45, 0x39 }, // '5' + { 0x3C, 0x4A, 0x49, 0x49, 0x30 }, // '6' + { 0x01, 0x71, 0x09, 0x05, 0x03 }, // '7' + { 0x36, 0x49, 0x49, 0x49, 0x36 }, // '8' + { 0x06, 0x49, 0x49, 0x29, 0x1E }, // '9' + { 0x00, 0x36, 0x36, 0x00, 0x00 }, // ':' + { 0x00, 0x56, 0x36, 0x00, 0x00 }, // ';' + { 0x00, 0x08, 0x14, 0x22, 0x41 }, // '<' + { 0x14, 0x14, 0x14, 0x14, 0x14 }, // '=' + { 0x41, 0x22, 0x14, 0x08, 0x00 }, // '>' + { 0x02, 0x01, 0x51, 0x09, 0x06 }, // '?' + { 0x32, 0x49, 0x79, 0x41, 0x3E }, // '@' + { 0x7E, 0x11, 0x11, 0x11, 0x7E }, // 'A' + { 0x7F, 0x49, 0x49, 0x49, 0x36 }, // 'B' + { 0x3E, 0x41, 0x41, 0x41, 0x22 }, // 'C' + { 0x7F, 0x41, 0x41, 0x22, 0x1C }, // 'D' + { 0x7F, 0x49, 0x49, 0x49, 0x41 }, // 'E' + { 0x7F, 0x09, 0x09, 0x09, 0x01 }, // 'F' + { 0x3E, 0x41, 0x49, 0x49, 0x7A }, // 'G' + { 0x7F, 0x08, 0x08, 0x08, 0x7F }, // 'H' + { 0x00, 0x41, 0x7F, 0x41, 0x00 }, // 'I' + { 0x20, 0x40, 0x41, 0x3F, 0x01 }, // 'J' + { 0x7F, 0x08, 0x14, 0x22, 0x41 }, // 'K' + { 0x7F, 0x40, 0x40, 0x40, 0x40 }, // 'L' + { 0x7F, 0x02, 0x04, 0x02, 0x7F }, // 'M' + { 0x7F, 0x04, 0x08, 0x10, 0x7F }, // 'N' + { 0x3E, 0x41, 0x41, 0x41, 0x3E }, // 'O' + { 0x7F, 0x09, 0x09, 0x09, 0x06 }, // 'P' + { 0x3E, 0x41, 0x51, 0x21, 0x5E }, // 'Q' + { 0x7F, 0x09, 0x19, 0x29, 0x46 }, // 'R' + { 0x46, 0x49, 0x49, 0x49, 0x31 }, // 'S' + { 0x01, 0x01, 0x7F, 0x01, 0x01 }, // 'T' + { 0x3F, 0x40, 0x40, 0x40, 0x3F }, // 'U' + { 0x1F, 0x20, 0x40, 0x20, 0x1F }, // 'V' + { 0x3F, 0x40, 0x38, 0x40, 0x3F }, // 'W' + { 0x63, 0x14, 0x08, 0x14, 0x63 }, // 'X' + { 0x07, 0x08, 0x70, 0x08, 0x07 }, // 'Y' + { 0x61, 0x51, 0x49, 0x45, 0x43 }, // 'Z' + { 0x00, 0x7F, 0x41, 0x41, 0x00 }, // '[' + { 0x02, 0x04, 0x08, 0x10, 0x20 }, // '\\' + { 0x00, 0x41, 0x41, 0x7F, 0x00 }, // ']' + { 0x04, 0x02, 0x01, 0x02, 0x04 }, // '^' + { 0x40, 0x40, 0x40, 0x40, 0x40 }, // '_' + { 0x00, 0x01, 0x02, 0x04, 0x00 }, // '`' + { 0x20, 0x54, 0x54, 0x54, 0x78 }, // 'a' + { 0x7F, 0x48, 0x44, 0x44, 0x38 }, // 'b' + { 0x38, 0x44, 0x44, 0x44, 0x20 }, // 'c' + { 0x38, 0x44, 0x44, 0x48, 0x7F }, // 'd' + { 0x38, 0x54, 0x54, 0x54, 0x18 }, // 'e' + { 0x08, 0x7E, 0x09, 0x01, 0x02 }, // 'f' + { 0x08, 0x14, 0x54, 0x54, 0x3C }, // 'g' + { 0x7F, 0x08, 0x04, 0x04, 0x78 }, // 'h' + { 0x00, 0x44, 0x7D, 0x40, 0x00 }, // 'i' + { 0x20, 0x40, 0x44, 0x3D, 0x00 }, // 'j' + { 0x7F, 0x10, 0x28, 0x44, 0x00 }, // 'k' + { 0x00, 0x41, 0x7F, 0x40, 0x00 }, // 'l' + { 0x7C, 0x04, 0x18, 0x04, 0x78 }, // 'm' + { 0x7C, 0x08, 0x04, 0x04, 0x78 }, // 'n' + { 0x38, 0x44, 0x44, 0x44, 0x38 }, // 'o' + { 0x7C, 0x14, 0x14, 0x14, 0x08 }, // 'p' + { 0x08, 0x14, 0x14, 0x18, 0x7C }, // 'q' + { 0x7C, 0x08, 0x04, 0x04, 0x08 }, // 'r' + { 0x48, 0x54, 0x54, 0x54, 0x20 }, // 's' + { 0x04, 0x3F, 0x44, 0x40, 0x20 }, // 't' + { 0x3C, 0x40, 0x40, 0x40, 0x7C }, // 'u' + { 0x1C, 0x20, 0x40, 0x20, 0x1C }, // 'v' + { 0x3C, 0x40, 0x30, 0x40, 0x3C }, // 'w' + { 0x44, 0x28, 0x10, 0x28, 0x44 }, // 'x' + { 0x0C, 0x50, 0x50, 0x50, 0x3C }, // 'y' + { 0x44, 0x64, 0x54, 0x4C, 0x44 }, // 'z' + { 0x00, 0x08, 0x36, 0x41, 0x00 }, // '{' + { 0x00, 0x00, 0x7F, 0x00, 0x00 }, // '|' + { 0x00, 0x41, 0x36, 0x08, 0x00 }, // '}' + { 0x08, 0x04, 0x08, 0x10, 0x08 }, // '~' +}; + +// Public-domain 8x8 font, ASCII 0-127. One byte per row, bit 0 is the leftmost pixel. +// Based on the public-domain IBM VGA 8x8 font via Daniel Hepper's font8x8_basic. +static const uint8_t FONT_8X8_DATA[128][8] = { + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00 }, // ! + { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // " + { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00 }, // # + { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00 }, // $ + { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00 }, // % + { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00 }, // & + { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ' + { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00 }, // ( + { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00 }, // ) + { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 }, // * + { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00 }, // + + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06 }, // , + { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00 }, // - + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 }, // . + { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00 }, // / + { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00 }, // 0 + { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00 }, // 1 + { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00 }, // 2 + { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00 }, // 3 + { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00 }, // 4 + { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00 }, // 5 + { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00 }, // 6 + { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00 }, // 7 + { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00 }, // 8 + { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00 }, // 9 + { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00 }, // : + { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06 }, // ; + { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00 }, // < + { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00 }, // = + { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00 }, // > + { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00 }, // ? + { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00 }, // @ + { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00 }, // A + { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00 }, // B + { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00 }, // C + { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00 }, // D + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00 }, // E + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00 }, // F + { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00 }, // G + { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00 }, // H + { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // I + { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00 }, // J + { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00 }, // K + { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00 }, // L + { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00 }, // M + { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00 }, // N + { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00 }, // O + { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00 }, // P + { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00 }, // Q + { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00 }, // R + { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00 }, // S + { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // T + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00 }, // U + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 }, // V + { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 }, // W + { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00 }, // X + { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00 }, // Y + { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00 }, // Z + { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00 }, // [ + { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00 }, // backslash + { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00 }, // ] + { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00 }, // ^ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }, // _ + { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` + { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00 }, // a + { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00 }, // b + { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00 }, // c + { 0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00 }, // d + { 0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00 }, // e + { 0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00 }, // f + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F }, // g + { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00 }, // h + { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // i + { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E }, // j + { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00 }, // k + { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // l + { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00 }, // m + { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00 }, // n + { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00 }, // o + { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F }, // p + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78 }, // q + { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00 }, // r + { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00 }, // s + { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00 }, // t + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00 }, // u + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 }, // v + { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00 }, // w + { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00 }, // x + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F }, // y + { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00 }, // z + { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00 }, // { + { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00 }, // | + { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00 }, // } + { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // DEL +}; + +const Font FONT_5X7 = { + 5, + 7, + 6, + 8, + 32, + 126, + FontLayout::Columns, + &FONT_5X7_DATA[0][0], +}; + +static const uint8_t FONT_5X7_PROPORTIONAL_WIDTHS[95] = { + 0, 3, 4, 5, 5, 5, 5, 3, 4, 4, 5, 5, 3, 5, 3, 5, 5, 4, 5, 5, + 5, 5, 5, 5, 5, 5, 3, 3, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, + 5, 4, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 3, 4, 5, +}; + +static const uint8_t FONT_5X7_PROPORTIONAL_ADVANCES[95] = { + 3, 4, 5, 6, 6, 6, 6, 4, 5, 5, 6, 6, 4, 6, 4, 6, 6, 5, 6, 6, + 6, 6, 6, 6, 6, 6, 4, 4, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, + 6, 5, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 4, 5, 6, +}; + +const Font FONT_5X7_PROPORTIONAL = { + 5, + 7, + 6, + 8, + 32, + 126, + FontLayout::Columns, + &FONT_5X7_DATA[0][0], + FONT_5X7_PROPORTIONAL_WIDTHS, + FONT_5X7_PROPORTIONAL_ADVANCES, +}; + +const Font FONT_8X8 = { + 8, + 8, + 8, + 8, + 0, + 127, + FontLayout::RowsLsb, + &FONT_8X8_DATA[0][0], +}; + +} // namespace gfx diff --git a/Libraries/Gfx/Source/Images.cpp b/Libraries/Gfx/Source/Images.cpp new file mode 100644 index 0000000..0da2a8c --- /dev/null +++ b/Libraries/Gfx/Source/Images.cpp @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "ColorUtils.h" + +#include + +namespace gfx { + +namespace { + +bool isFastRgb565Format(enum DisplayColorFormat format) { + switch (format) { + case DISPLAY_COLOR_FORMAT_RGB565: + case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: + case DISPLAY_COLOR_FORMAT_BGR565: + case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: + return true; + default: + return false; + } +} + +int bytesPerPixel(enum DisplayColorFormat format) { + switch (format) { + case DISPLAY_COLOR_FORMAT_RGB888: + return 3; + case DISPLAY_COLOR_FORMAT_GRAYSCALE8: + return 1; + case DISPLAY_COLOR_FORMAT_MONOCHROME: + return 0; + default: + return 2; + } +} + +} // namespace + +void Surface::blit(const struct PixelBuffer& source, int sourceX, int sourceY, int width, int height, int destX, int destY, enum PixelBufferConversion conversion) { + const int sourceWidth = pixel_buffer_get_width(&source); + const int sourceHeight = pixel_buffer_get_height(&source); + + int sx = sourceX; + int sy = sourceY; + int dx = destX; + int dy = destY; + int w = width; + int h = height; + + if (sx < 0) { + dx -= sx; + w += sx; + sx = 0; + } + if (sy < 0) { + dy -= sy; + h += sy; + sy = 0; + } + if (sx + w > sourceWidth) { + w = sourceWidth - sx; + } + if (sy + h > sourceHeight) { + h = sourceHeight - sy; + } + + if (dx < clip_.x0) { + const int delta = clip_.x0 - dx; + sx += delta; + w -= delta; + dx = clip_.x0; + } + if (dy < clip_.y0) { + const int delta = clip_.y0 - dy; + sy += delta; + h -= delta; + dy = clip_.y0; + } + if (dx + w > clip_.x1) { + w = clip_.x1 - dx; + } + if (dy + h > clip_.y1) { + h = clip_.y1 - dy; + } + if (w <= 0 || h <= 0) { + return; + } + + markDirtyRect(dx, dy, dx + w, dy + h); + + const auto sourceFormat = pixel_buffer_get_format(&source); + if (sourceFormat == format_ && sourceFormat != DISPLAY_COLOR_FORMAT_MONOCHROME) { + const int bpp = bytesPerPixel(sourceFormat); + const auto* sourceData = static_cast(pixel_buffer_get_data(const_cast(&source))); + const std::size_t sourceStride = pixel_buffer_get_stride_bytes(&source); + const int localX = dx - bounds_.x0; + const int localY = dy - bounds_.y0; + + for (int row = 0; row < h; row++) { + std::memcpy( + data_ + static_cast(localY + row) * stride_ + static_cast(localX) * bpp, + sourceData + static_cast(sy + row) * sourceStride + static_cast(sx) * bpp, + static_cast(w) * bpp + ); + } + return; + } + + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + const uint16_t rgb565 = pixel_buffer_get_pixel_rgb565(&source, sx + col, sy + row); + writePixel(dx + col - bounds_.x0, dy + row - bounds_.y0, rgb565, conversion); + } + } +} + +void Surface::blitTransparent(const struct PixelBuffer& source, int sourceX, int sourceY, int width, int height, int destX, int destY, uint16_t transparentRgb565, enum PixelBufferConversion conversion) { + const int sourceWidth = pixel_buffer_get_width(&source); + const int sourceHeight = pixel_buffer_get_height(&source); + + int sx = sourceX; + int sy = sourceY; + int dx = destX; + int dy = destY; + int w = width; + int h = height; + + if (sx < 0) { + dx -= sx; + w += sx; + sx = 0; + } + if (sy < 0) { + dy -= sy; + h += sy; + sy = 0; + } + if (sx + w > sourceWidth) { + w = sourceWidth - sx; + } + if (sy + h > sourceHeight) { + h = sourceHeight - sy; + } + + if (dx < clip_.x0) { + const int delta = clip_.x0 - dx; + sx += delta; + w -= delta; + dx = clip_.x0; + } + if (dy < clip_.y0) { + const int delta = clip_.y0 - dy; + sy += delta; + h -= delta; + dy = clip_.y0; + } + if (dx + w > clip_.x1) { + w = clip_.x1 - dx; + } + if (dy + h > clip_.y1) { + h = clip_.y1 - dy; + } + if (w <= 0 || h <= 0) { + return; + } + + markDirtyRect(dx, dy, dx + w, dy + h); + + const auto sourceFormat = pixel_buffer_get_format(&source); + if (sourceFormat == format_ && isFastRgb565Format(sourceFormat)) { + const uint16_t transparentNative = detail::encode565(sourceFormat, transparentRgb565); + const auto* sourceData = static_cast(pixel_buffer_get_data(const_cast(&source))); + const std::size_t sourceStride = pixel_buffer_get_stride_bytes(&source); + const int localX = dx - bounds_.x0; + const int localY = dy - bounds_.y0; + + for (int row = 0; row < h; row++) { + const auto* sourceRow = reinterpret_cast( + sourceData + static_cast(sy + row) * sourceStride + ); + auto* destinationRow = reinterpret_cast( + data_ + static_cast(localY + row) * stride_ + ); + for (int col = 0; col < w; col++) { + const uint16_t value = sourceRow[sx + col]; + if (value != transparentNative) { + destinationRow[localX + col] = value; + } + } + } + return; + } + + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + const uint16_t rgb565 = pixel_buffer_get_pixel_rgb565(&source, sx + col, sy + row); + if (rgb565 != transparentRgb565) { + writePixel(dx + col - bounds_.x0, dy + row - bounds_.y0, rgb565, conversion); + } + } + } +} + +void Surface::drawMask(const uint8_t* mask, int maskStride, int width, int height, int x, int y, uint16_t rgb565, bool msbFirst) { + if (mask == nullptr || width <= 0 || height <= 0) { + return; + } + if (maskStride <= 0) { + maskStride = (width + 7) / 8; + } + + int maskX = 0; + int maskY = 0; + int dx = x; + int dy = y; + int w = width; + int h = height; + + if (dx < clip_.x0) { + const int delta = clip_.x0 - dx; + maskX += delta; + w -= delta; + dx = clip_.x0; + } + if (dy < clip_.y0) { + const int delta = clip_.y0 - dy; + maskY += delta; + h -= delta; + dy = clip_.y0; + } + if (dx + w > clip_.x1) { + w = clip_.x1 - dx; + } + if (dy + h > clip_.y1) { + h = clip_.y1 - dy; + } + if (w <= 0 || h <= 0) { + return; + } + + markDirtyRect(dx, dy, dx + w, dy + h); + + for (int row = 0; row < h; row++) { + const uint8_t* maskRow = mask + static_cast(maskY + row) * maskStride; + for (int col = 0; col < w; col++) { + const int sourceCol = maskX + col; + const uint8_t value = maskRow[sourceCol >> 3]; + const uint8_t bit = msbFirst + ? static_cast(0x80U >> (sourceCol & 7)) + : static_cast(1U << (sourceCol & 7)); + if (value & bit) { + plotRaw(dx + col, dy + row, rgb565); + } + } + } +} + +void Surface::drawAlphaMask(const uint8_t* alpha, int alphaStride, int width, int height, int x, int y, uint16_t rgb565) { + if (alpha == nullptr || width <= 0 || height <= 0) { + return; + } + if (alphaStride <= 0) { + alphaStride = width; + } + + int maskX = 0; + int maskY = 0; + int dx = x; + int dy = y; + int w = width; + int h = height; + + if (dx < clip_.x0) { + const int delta = clip_.x0 - dx; + maskX += delta; + w -= delta; + dx = clip_.x0; + } + if (dy < clip_.y0) { + const int delta = clip_.y0 - dy; + maskY += delta; + h -= delta; + dy = clip_.y0; + } + if (dx + w > clip_.x1) { + w = clip_.x1 - dx; + } + if (dy + h > clip_.y1) { + h = clip_.y1 - dy; + } + if (w <= 0 || h <= 0) { + return; + } + + markDirtyRect(dx, dy, dx + w, dy + h); + + for (int row = 0; row < h; row++) { + const uint8_t* alphaRow = alpha + static_cast(maskY + row) * alphaStride; + for (int col = 0; col < w; col++) { + const uint8_t value = alphaRow[maskX + col]; + if (value != 0) { + blendPixelRaw(dx + col, dy + row, rgb565, value); + } + } + } +} + +void Surface::blendPixelRaw(int x, int y, uint16_t rgb565, uint8_t alpha) { + if (alpha == 0 || !clip_.contains(x, y)) { + return; + } + + const int localX = x - bounds_.x0; + const int localY = y - bounds_.y0; + if (alpha == 255) { + writePixel(localX, localY, rgb565); + return; + } + + const uint16_t destination = readPixel(localX, localY); + writePixel(localX, localY, detail::blend565(destination, rgb565, alpha)); +} + +void Surface::blendPixel(int x, int y, uint16_t rgb565, uint8_t alpha) { + if (alpha == 0 || !clip_.contains(x, y)) { + return; + } + blendPixelRaw(x, y, rgb565, alpha); + markDirtyRect(x, y, x + 1, y + 1); +} + +} // namespace gfx diff --git a/Libraries/Gfx/Source/Polygon.cpp b/Libraries/Gfx/Source/Polygon.cpp new file mode 100644 index 0000000..4decd9d --- /dev/null +++ b/Libraries/Gfx/Source/Polygon.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +namespace gfx { + +namespace { + +// Local rounding avoids lroundf, which the current firmware doesn't export to app ELFs. +// Replace with std::lround()/lroundf() once the c-symbols module exports it (see DESIGN.md, +// "Symbol export notes"). +int roundToInt(float value) { + return static_cast(value >= 0.0f ? value + 0.5f : value - 0.5f); +} + +} // namespace + +void Surface::drawPolygon(const Point* points, int count, uint16_t rgb565) { + if (points == nullptr || count < 2 || count > MAX_POLYGON_POINTS) { + return; + } + + int minX = points[0].x; + int maxX = points[0].x; + int minY = points[0].y; + int maxY = points[0].y; + for (int i = 1; i < count; i++) { + minX = std::min(minX, points[i].x); + maxX = std::max(maxX, points[i].x); + minY = std::min(minY, points[i].y); + maxY = std::max(maxY, points[i].y); + } + + const Rect box = Rect { minX, minY, maxX + 1, maxY + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + for (int i = 0; i < count; i++) { + const Point& a = points[i]; + const Point& b = points[(i + 1) % count]; + lineRaw(a.x, a.y, b.x, b.y, rgb565); + } +} + +void Surface::fillPolygon(const Point* points, int count, uint16_t rgb565) { + if (points == nullptr || count < 3 || count > MAX_POLYGON_POINTS) { + return; + } + + int minX = points[0].x; + int maxX = points[0].x; + int minY = points[0].y; + int maxY = points[0].y; + for (int i = 1; i < count; i++) { + minX = std::min(minX, points[i].x); + maxX = std::max(maxX, points[i].x); + minY = std::min(minY, points[i].y); + maxY = std::max(maxY, points[i].y); + } + + const Rect box = Rect { minX, minY, maxX + 1, maxY + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + const int startY = std::max(minY, clip_.y0); + const int endY = std::min(maxY, clip_.y1 - 1); + int intersections[MAX_POLYGON_POINTS]; + + for (int y = startY; y <= endY; y++) { + // Sample the middle of the row so vertices don't count twice. + const float scanY = static_cast(y) + 0.5f; + int intersectionCount = 0; + + for (int i = 0; i < count; i++) { + const Point& a = points[i]; + const Point& b = points[(i + 1) % count]; + if (a.y == b.y) { + continue; + } + + const int lowY = std::min(a.y, b.y); + const int highY = std::max(a.y, b.y); + if (scanY < lowY || scanY >= highY) { + continue; + } + + const float t = (scanY - static_cast(a.y)) / static_cast(b.y - a.y); + intersections[intersectionCount++] = roundToInt(static_cast(a.x) + t * static_cast(b.x - a.x)); + } + + std::sort(intersections, intersections + intersectionCount); + for (int i = 0; i + 1 < intersectionCount; i += 2) { + const int x0 = intersections[i]; + const int x1 = intersections[i + 1]; + if (x0 < x1) { + fillSpanRaw(x0, x1, y, rgb565); + } + } + } +} + +} // namespace gfx diff --git a/Libraries/Gfx/Source/Shapes.cpp b/Libraries/Gfx/Source/Shapes.cpp new file mode 100644 index 0000000..85fc843 --- /dev/null +++ b/Libraries/Gfx/Source/Shapes.cpp @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include + +namespace gfx { + +namespace { + +constexpr double DEGREES_TO_RADIANS = 3.14159265358979323846 / 180.0; + +/** Absorbs the rounding error of sin/cos when a point lies exactly on an arc boundary. */ +constexpr double ARC_ANGLE_EPSILON = 1e-6; + +float normaliseDegrees(float degrees) { + degrees = std::fmod(degrees, 360.0f); + if (degrees < 0.0f) { + degrees += 360.0f; + } + return degrees; +} + +/** Rounds a value to the nearest integer, for arc endpoints. */ +int roundToInt(float value) { + return static_cast(value >= 0.0f ? value + 0.5f : value - 0.5f); +} + +} // namespace + +void Surface::fillCircle(int cx, int cy, int radius, uint16_t rgb565) { + if (radius < 0) { + return; + } + + const Rect box = Rect { cx - radius, cy - radius, cx + radius + 1, cy + radius + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + if (radius == 0) { + fillSpanRaw(cx, cx + 1, cy, rgb565); + return; + } + + const int64_t radius_squared = static_cast(radius) * radius; + int x = radius; + for (int y = 0; y <= radius; y++) { + while (x > 0 && static_cast(x) * x + static_cast(y) * y > radius_squared) { + x--; + } + fillSpanRaw(cx - x, cx + x + 1, cy + y, rgb565); + if (y != 0) { + fillSpanRaw(cx - x, cx + x + 1, cy - y, rgb565); + } + } +} + +void Surface::drawCircle(int cx, int cy, int radius, uint16_t rgb565) { + if (radius < 0) { + return; + } + + const Rect box = Rect { cx - radius, cy - radius, cx + radius + 1, cy + radius + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + if (radius == 0) { + plotRaw(cx, cy, rgb565); + return; + } + + auto plotOctants = [&](int x, int y) { + plotRaw(cx + x, cy + y, rgb565); + plotRaw(cx - x, cy + y, rgb565); + plotRaw(cx + x, cy - y, rgb565); + plotRaw(cx - x, cy - y, rgb565); + plotRaw(cx + y, cy + x, rgb565); + plotRaw(cx - y, cy + x, rgb565); + plotRaw(cx + y, cy - x, rgb565); + plotRaw(cx - y, cy - x, rgb565); + }; + + int x = radius; + int y = 0; + int error = 1 - radius; + while (x >= y) { + plotOctants(x, y); + y++; + if (error < 0) { + error += 2 * y + 1; + } else { + x--; + error += 2 * (y - x) + 1; + } + } +} + +void Surface::fillEllipse(int cx, int cy, int radiusX, int radiusY, uint16_t rgb565) { + if (radiusX < 0 || radiusY < 0) { + return; + } + + const Rect box = Rect { cx - radiusX, cy - radiusY, cx + radiusX + 1, cy + radiusY + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + if (radiusX == 0 && radiusY == 0) { + plotRaw(cx, cy, rgb565); + return; + } + if (radiusX == 0) { + fillRectRaw(Rect { cx, cy - radiusY, cx + 1, cy + radiusY + 1 }, rgb565); + return; + } + if (radiusY == 0) { + fillSpanRaw(cx - radiusX, cx + radiusX + 1, cy, rgb565); + return; + } + + const int64_t rx2 = static_cast(radiusX) * radiusX; + const int64_t ry2 = static_cast(radiusY) * radiusY; + const int64_t limit = rx2 * ry2; + + int x = radiusX; + for (int y = 0; y <= radiusY; y++) { + while (x > 0 && static_cast(x) * x * ry2 + static_cast(y) * y * rx2 > limit) { + x--; + } + fillSpanRaw(cx - x, cx + x + 1, cy + y, rgb565); + if (y != 0) { + fillSpanRaw(cx - x, cx + x + 1, cy - y, rgb565); + } + } +} + +void Surface::drawEllipse(int cx, int cy, int radiusX, int radiusY, uint16_t rgb565) { + if (radiusX < 0 || radiusY < 0) { + return; + } + + const Rect box = Rect { cx - radiusX, cy - radiusY, cx + radiusX + 1, cy + radiusY + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + if (radiusX == 0 && radiusY == 0) { + plotRaw(cx, cy, rgb565); + return; + } + if (radiusX == 0) { + lineRaw(cx, cy - radiusY, cx, cy + radiusY, rgb565); + return; + } + if (radiusY == 0) { + lineRaw(cx - radiusX, cy, cx + radiusX, cy, rgb565); + return; + } + + const int64_t rx2 = static_cast(radiusX) * radiusX; + const int64_t ry2 = static_cast(radiusY) * radiusY; + const int64_t limit = rx2 * ry2; + + auto plotQuadrants = [&](int x, int y) { + plotRaw(cx + x, cy + y, rgb565); + plotRaw(cx - x, cy + y, rgb565); + plotRaw(cx + x, cy - y, rgb565); + plotRaw(cx - x, cy - y, rgb565); + }; + + // Walk the curve both as x(y) and as y(x). The union covers the nearly-horizontal and + // nearly-vertical parts, which either walk on its own would leave with gaps. + int x = radiusX; + for (int y = 0; y <= radiusY; y++) { + while (x > 0 && static_cast(x) * x * ry2 + static_cast(y) * y * rx2 > limit) { + x--; + } + plotQuadrants(x, y); + } + + int y = radiusY; + for (int sample_x = 0; sample_x <= radiusX; sample_x++) { + while (y > 0 && static_cast(sample_x) * sample_x * ry2 + static_cast(y) * y * rx2 > limit) { + y--; + } + plotQuadrants(sample_x, y); + } +} + +void Surface::fillRoundRect(int x, int y, int width, int height, int radius, uint16_t rgb565) { + if (width <= 0 || height <= 0) { + return; + } + if (radius <= 0) { + fillRect(x, y, width, height, rgb565); + return; + } + + radius = std::min(radius, std::min(width, height) / 2); + if (radius <= 0) { + fillRect(x, y, width, height, rgb565); + return; + } + + const Rect box = Rect { x, y, x + width, y + height }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + // The centre rectangle and the two straight strips between the corners. + fillRectRaw(Rect { x, y + radius, x + width, y + height - radius }, rgb565); + fillRectRaw(Rect { x + radius, y, x + width - radius, y + radius }, rgb565); + fillRectRaw(Rect { x + radius, y + height - radius, x + width - radius, y + height }, rgb565); + + const int64_t radius_squared = static_cast(radius) * radius; + int dx = 0; + for (int row = 0; row < radius; row++) { + const int dy = radius - row; + while (dx < radius && static_cast(dx + 1) * (dx + 1) + static_cast(dy) * dy <= radius_squared) { + dx++; + } + const int inset = radius - dx; + fillSpanRaw(x + inset, x + width - inset, y + row, rgb565); + fillSpanRaw(x + inset, x + width - inset, y + height - 1 - row, rgb565); + } +} + +void Surface::drawRoundRect(int x, int y, int width, int height, int radius, uint16_t rgb565) { + if (width <= 0 || height <= 0) { + return; + } + if (radius <= 0) { + drawRect(x, y, width, height, rgb565); + return; + } + + radius = std::min(radius, std::min(width, height) / 2); + if (radius <= 0) { + drawRect(x, y, width, height, rgb565); + return; + } + + const Rect box = Rect { x, y, x + width, y + height }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + fillSpanRaw(x + radius, x + width - radius, y, rgb565); + fillSpanRaw(x + radius, x + width - radius, y + height - 1, rgb565); + fillRectRaw(Rect { x, y + radius, x + 1, y + height - radius }, rgb565); + fillRectRaw(Rect { x + width - 1, y + radius, x + width, y + height - radius }, rgb565); + + const int64_t radius_squared = static_cast(radius) * radius; + int dx = 0; + for (int row = 0; row < radius; row++) { + const int dy = radius - row; + while (dx < radius && static_cast(dx + 1) * (dx + 1) + static_cast(dy) * dy <= radius_squared) { + dx++; + } + const int inset = radius - dx; + plotRaw(x + inset, y + row, rgb565); + plotRaw(x + width - 1 - inset, y + row, rgb565); + plotRaw(x + inset, y + height - 1 - row, rgb565); + plotRaw(x + width - 1 - inset, y + height - 1 - row, rgb565); + } +} + +void Surface::fillTriangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t rgb565) { + // Sort the vertices by y so the scanline fill can run top to bottom. + if (y0 > y1) { + std::swap(x0, x1); + std::swap(y0, y1); + } + if (y1 > y2) { + std::swap(x1, x2); + std::swap(y1, y2); + } + if (y0 > y1) { + std::swap(x0, x1); + std::swap(y0, y1); + } + + const int min_x = std::min({ x0, x1, x2 }); + const int max_x = std::max({ x0, x1, x2 }); + const Rect box = Rect { min_x, y0, max_x + 1, y2 + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + if (y0 == y2) { + fillSpanRaw(min_x, max_x + 1, y0, rgb565); + return; + } + + // Fixed-point slopes (16 subpixel bits) keep the edges stable without floating point. + const int32_t dx02 = (static_cast(x2 - x0) << 4) / (y2 - y0); + int32_t xa = static_cast(x0) << 4; + + if (y1 == y0) { + const int32_t dx12 = (static_cast(x2 - x1) << 4) / (y2 - y1); + int32_t xb = static_cast(x1) << 4; + for (int y = y0; y <= y2; y++) { + int low = static_cast(xa >> 4); + int high = static_cast(xb >> 4); + if (low > high) { + std::swap(low, high); + } + fillSpanRaw(low, high + 1, y, rgb565); + xa += dx02; + xb += dx12; + } + return; + } + + if (y1 == y2) { + const int32_t dx01 = (static_cast(x1 - x0) << 4) / (y1 - y0); + int32_t xb = static_cast(x0) << 4; + for (int y = y0; y <= y1; y++) { + int low = static_cast(xa >> 4); + int high = static_cast(xb >> 4); + if (low > high) { + std::swap(low, high); + } + fillSpanRaw(low, high + 1, y, rgb565); + xa += dx02; + xb += dx01; + } + return; + } + + const int32_t dx01 = (static_cast(x1 - x0) << 4) / (y1 - y0); + int32_t xb = static_cast(x0) << 4; + for (int y = y0; y < y1; y++) { + int low = static_cast(xa >> 4); + int high = static_cast(xb >> 4); + if (low > high) { + std::swap(low, high); + } + fillSpanRaw(low, high + 1, y, rgb565); + xa += dx02; + xb += dx01; + } + + const int32_t dx12 = (static_cast(x2 - x1) << 4) / (y2 - y1); + xb = static_cast(x1) << 4; + for (int y = y1; y <= y2; y++) { + int low = static_cast(xa >> 4); + int high = static_cast(xb >> 4); + if (low > high) { + std::swap(low, high); + } + fillSpanRaw(low, high + 1, y, rgb565); + xa += dx02; + xb += dx12; + } +} + +void Surface::drawTriangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t rgb565) { + const int min_x = std::min({ x0, x1, x2 }); + const int max_x = std::max({ x0, x1, x2 }); + const int min_y = std::min({ y0, y1, y2 }); + const int max_y = std::max({ y0, y1, y2 }); + const Rect box = Rect { min_x, min_y, max_x + 1, max_y + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + lineRaw(x0, y0, x1, y1, rgb565); + lineRaw(x1, y1, x2, y2, rgb565); + lineRaw(x2, y2, x0, y0, rgb565); +} + +void Surface::fillArc(int cx, int cy, int outerRadius, int innerRadius, float startDegrees, float endDegrees, uint16_t rgb565) { + if (outerRadius <= 0) { + return; + } + if (innerRadius < 0) { + innerRadius = 0; + } + if (innerRadius > outerRadius) { + std::swap(outerRadius, innerRadius); + } + + const Rect box = Rect { cx - outerRadius, cy - outerRadius, cx + outerRadius + 1, cy + outerRadius + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + const float start = normaliseDegrees(startDegrees); + const float end = normaliseDegrees(endDegrees); + float sweep = end - start; + if (sweep <= 0.0f) { + sweep += 360.0f; + } + const bool full = sweep >= 359.999f; + const bool wide = sweep > 180.0f; + + const double start_radians = start * DEGREES_TO_RADIANS; + const double end_radians = end * DEGREES_TO_RADIANS; + const double start_sin = std::sin(start_radians); + const double start_cos = std::cos(start_radians); + const double end_sin = std::sin(end_radians); + const double end_cos = std::cos(end_radians); + + const int64_t outer_squared = static_cast(outerRadius) * outerRadius; + const int64_t inner_squared = static_cast(innerRadius) * innerRadius; + + for (int y = box.y0; y < box.y1; y++) { + const int dy = y - cy; + for (int x = box.x0; x < box.x1; x++) { + const int dx = x - cx; + const int64_t distance_squared = static_cast(dx) * dx + static_cast(dy) * dy; + if (distance_squared > outer_squared || distance_squared < inner_squared) { + continue; + } + + if (!full) { + const double cross_start = start_cos * dy - start_sin * dx; + const double cross_end = dx * end_sin - dy * end_cos; + const bool inside = wide ? (cross_start >= -ARC_ANGLE_EPSILON || cross_end >= -ARC_ANGLE_EPSILON) : (cross_start >= -ARC_ANGLE_EPSILON && cross_end >= -ARC_ANGLE_EPSILON); + if (!inside) { + continue; + } + } + + plotRaw(x, y, rgb565); + } + } +} + +void Surface::drawArc(int cx, int cy, int outerRadius, int innerRadius, float startDegrees, float endDegrees, uint16_t rgb565) { + if (outerRadius <= 0) { + return; + } + if (innerRadius < 0) { + innerRadius = 0; + } + if (innerRadius > outerRadius) { + std::swap(outerRadius, innerRadius); + } + + const Rect box = Rect { cx - outerRadius, cy - outerRadius, cx + outerRadius + 1, cy + outerRadius + 1 }.intersect(clip_); + if (box.empty()) { + return; + } + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + const float start = normaliseDegrees(startDegrees); + const float end = normaliseDegrees(endDegrees); + float sweep = end - start; + if (sweep <= 0.0f) { + sweep += 360.0f; + } + const bool full = sweep >= 359.999f; + const bool wide = sweep > 180.0f; + + const double start_radians = start * DEGREES_TO_RADIANS; + const double end_radians = end * DEGREES_TO_RADIANS; + const double start_sin = std::sin(start_radians); + const double start_cos = std::cos(start_radians); + const double end_sin = std::sin(end_radians); + const double end_cos = std::cos(end_radians); + + const int64_t outer_squared = static_cast(outerRadius) * outerRadius; + const int64_t inner_outer_squared = static_cast(outerRadius - 1) * (outerRadius - 1); + const int64_t inner_squared = static_cast(innerRadius) * innerRadius; + const int64_t inner_ring_squared = static_cast(innerRadius + 1) * (innerRadius + 1); + + for (int y = box.y0; y < box.y1; y++) { + const int dy = y - cy; + for (int x = box.x0; x < box.x1; x++) { + const int dx = x - cx; + const int64_t distance_squared = static_cast(dx) * dx + static_cast(dy) * dy; + + const bool on_outer = distance_squared <= outer_squared && distance_squared > inner_outer_squared; + const bool on_inner = innerRadius > 0 && distance_squared >= inner_squared && distance_squared < inner_ring_squared; + if (!on_outer && !on_inner) { + continue; + } + + if (!full) { + const double cross_start = start_cos * dy - start_sin * dx; + const double cross_end = dx * end_sin - dy * end_cos; + const bool inside = wide ? (cross_start >= -ARC_ANGLE_EPSILON || cross_end >= -ARC_ANGLE_EPSILON) : (cross_start >= -ARC_ANGLE_EPSILON && cross_end >= -ARC_ANGLE_EPSILON); + if (!inside) { + continue; + } + } + + plotRaw(x, y, rgb565); + } + } + + if (!full) { + // Cap the two radial ends so an annulus reads as a wedge rather than two floating rings. + const int outer_start_x = cx + roundToInt(static_cast(outerRadius * start_cos)); + const int outer_start_y = cy + roundToInt(static_cast(outerRadius * start_sin)); + const int inner_start_x = cx + roundToInt(static_cast(innerRadius * start_cos)); + const int inner_start_y = cy + roundToInt(static_cast(innerRadius * start_sin)); + const int outer_end_x = cx + roundToInt(static_cast(outerRadius * end_cos)); + const int outer_end_y = cy + roundToInt(static_cast(outerRadius * end_sin)); + const int inner_end_x = cx + roundToInt(static_cast(innerRadius * end_cos)); + const int inner_end_y = cy + roundToInt(static_cast(innerRadius * end_sin)); + + lineRaw(outer_start_x, outer_start_y, inner_start_x, inner_start_y, rgb565); + lineRaw(outer_end_x, outer_end_y, inner_end_x, inner_end_y, rgb565); + } +} + +} // namespace gfx diff --git a/Libraries/Gfx/Source/SinkTarget.cpp b/Libraries/Gfx/Source/SinkTarget.cpp new file mode 100644 index 0000000..c96d49b --- /dev/null +++ b/Libraries/Gfx/Source/SinkTarget.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#include + +#ifdef ESP_PLATFORM +#include +#endif + +namespace gfx { + +namespace { + +constexpr auto* TAG = "gfx"; + +int64_t nowUs() { +#ifdef ESP_PLATFORM + return esp_timer_get_time(); +#else + return 0; +#endif +} + +} // namespace + +SinkTarget::SinkTarget(Sink& sink, int width, int height, enum DisplayColorFormat format) + : sink_(sink) { + if (width <= 0 || height <= 0) { + return; + } + + frame_buffer_ = nullptr; + + MemoryPolicy policy {}; + if (sink_.frame_buffer_policy(policy)) { + frame_buffer_ = pixel_buffer_create_with_policy(format, width, height, &policy); + } else { + frame_buffer_ = pixel_buffer_create(format, width, height); + } + if (frame_buffer_ == nullptr) { + LOG_E(TAG, "Failed to allocate a %dx%d frame buffer for the sink", width, height); + return; + } + + const SinkInfo sink_info { width, height, format }; + if (!sink_.begin(sink_info)) { + LOG_E(TAG, "Sink rejected a %dx%d format-%d frame", width, height, static_cast(format)); + pixel_buffer_free(frame_buffer_); + frame_buffer_ = nullptr; + return; + } + + begun_ = true; + info_.width = width; + info_.height = height; + info_.format = format; + info_.mode = RenderMode::FullFrame; + info_.frameWidth = width; + info_.frameHeight = height; + info_.bandHeight = height; + info_.slowRefresh = false; + info_.fullFrameRequired = false; + info_.hardwareFrameBuffer = false; +} + +SinkTarget::~SinkTarget() { + if (begun_) { + sink_.end(); + } + if (frame_buffer_ != nullptr) { + pixel_buffer_free(frame_buffer_); + } +} + +void SinkTarget::render(RenderCallback callback, void* context, Prepare prepare) { + (void)prepare; + + if (!valid() || callback == nullptr) { + return; + } + + const int64_t t0 = nowUs(); + + dirty_.clear(); + Surface surface(frame_buffer_, 0, info_.height, &dirty_); + callback(surface, context); + + const int64_t t1 = nowUs(); + + auto present = [&](const Rect& rect) { + const Rect clipped = rect.intersect(Rect { 0, 0, info_.width, info_.height }); + if (clipped.empty()) { + return; + } + + // Full rows are safe for every format (including monochrome), and sinks can use the + // buffer's stride directly. + pixel_buffer_msync(frame_buffer_, 0, clipped.y0, info_.width, clipped.height()); + if (!sink_.present(*frame_buffer_, clipped)) { + LOG_W(TAG, "Sink rejected a %dx%d region at (%d, %d)", clipped.width(), clipped.height(), clipped.x0, clipped.y0); + } + }; + + if (first_frame_) { + present(Rect { 0, 0, info_.width, info_.height }); + first_frame_ = false; + stats_.dirtyRects = 1; + stats_.dirtyPixels = info_.width * info_.height; + } else { + stats_.dirtyRects = 0; + stats_.dirtyPixels = 0; + for (int i = 0; i < dirty_.count(); i++) { + const Rect rect = dirty_.at(i).intersect(Rect { 0, 0, info_.width, info_.height }); + if (rect.empty()) { + continue; + } + present(rect); + stats_.dirtyRects++; + stats_.dirtyPixels += rect.width() * rect.height(); + } + } + + const int64_t t2 = nowUs(); + stats_.copyUs = 0; + stats_.drawUs = static_cast(t1 - t0); + stats_.syncUs = 0; + stats_.presentUs = static_cast(t2 - t1); + stats_.totalUs = static_cast(t2 - t0); +} + +} // namespace gfx diff --git a/Libraries/Gfx/Source/Surface.cpp b/Libraries/Gfx/Source/Surface.cpp new file mode 100644 index 0000000..3a8795f --- /dev/null +++ b/Libraries/Gfx/Source/Surface.cpp @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "ColorUtils.h" +#include "gfx/DirtyRegions.h" + +#include +#include + +namespace gfx { + +Surface::Surface(struct PixelBuffer* buffer) { + if (buffer == nullptr) { + return; + } + + const int width = pixel_buffer_get_width(buffer); + const int height = pixel_buffer_get_height(buffer); + initialise(buffer, Rect { 0, 0, width, height }, static_cast(pixel_buffer_get_data(buffer)), nullptr); +} + +Surface::Surface(struct PixelBuffer* buffer, const Rect& view) { + if (buffer == nullptr) { + return; + } + + const auto format = pixel_buffer_get_format(buffer); + const int buffer_width = pixel_buffer_get_width(buffer); + const int buffer_height = pixel_buffer_get_height(buffer); + const Rect clipped = view.intersect(Rect { 0, 0, buffer_width, buffer_height }); + if (clipped.empty()) { + return; + } + + const std::size_t stride = pixel_buffer_get_stride_bytes(buffer); + const std::size_t x_offset = pixel_buffer_row_stride_bytes(format, clipped.x0); + auto* data = static_cast(pixel_buffer_get_data(buffer)) + static_cast(clipped.y0) * stride + x_offset; + + initialise(buffer, Rect { 0, 0, clipped.width(), clipped.height() }, data, nullptr); + if (format == DISPLAY_COLOR_FORMAT_MONOCHROME) { + // A sub-view can start inside a byte. writePixel() shifts by this amount for monochrome. + x_offset_bits_ = clipped.x0 & 7; + } +} + +Surface::Surface(struct PixelBuffer* buffer, int originY, int rows, DirtyRegions* dirty) { + if (buffer == nullptr) { + return; + } + + const int width = pixel_buffer_get_width(buffer); + initialise(buffer, Rect { 0, originY, width, originY + rows }, static_cast(pixel_buffer_get_data(buffer)), dirty); +} + +void Surface::initialise(struct PixelBuffer* buffer, const Rect& bounds, uint8_t* data, DirtyRegions* dirty) { + buffer_ = buffer; + data_ = data; + stride_ = pixel_buffer_get_stride_bytes(buffer); + bounds_ = bounds; + clip_ = bounds; + format_ = pixel_buffer_get_format(buffer); + dirty_ = dirty; +} + +void Surface::setClip(const Rect& clip) { + clip_ = clip.intersect(bounds_); +} + +bool Surface::isFastRgb565() const { + switch (format_) { + case DISPLAY_COLOR_FORMAT_RGB565: + case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: + case DISPLAY_COLOR_FORMAT_BGR565: + case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: + return true; + default: + return false; + } +} + +uint16_t Surface::toNative565(uint16_t rgb565) const { + return detail::encode565(format_, rgb565); +} + +void Surface::writePixel(int x, int local_y, uint16_t rgb565) { + writePixel(x, local_y, rgb565, PIXEL_BUFFER_CONVERSION_EXACT_BLACK); +} + +void Surface::writePixel(int x, int local_y, uint16_t rgb565, enum PixelBufferConversion conversion) { + uint8_t* row = data_ + static_cast(local_y) * stride_; + switch (format_) { + case DISPLAY_COLOR_FORMAT_RGB565: + reinterpret_cast(row)[x] = rgb565; + break; + case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: + reinterpret_cast(row)[x] = detail::byteswap16(rgb565); + break; + case DISPLAY_COLOR_FORMAT_BGR565: + reinterpret_cast(row)[x] = detail::swapRedBlue(rgb565); + break; + case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: + reinterpret_cast(row)[x] = detail::byteswap16(detail::swapRedBlue(rgb565)); + break; + case DISPLAY_COLOR_FORMAT_RGB888: { + uint8_t rgb[3]; + detail::toRgb888(rgb565, rgb); + row[x * 3] = rgb[0]; + row[x * 3 + 1] = rgb[1]; + row[x * 3 + 2] = rgb[2]; + break; + } + case DISPLAY_COLOR_FORMAT_GRAYSCALE8: + row[x] = detail::toGrayscale(rgb565, conversion); + break; + case DISPLAY_COLOR_FORMAT_MONOCHROME: + default: { + const int shifted = x + x_offset_bits_; + const uint8_t mask = static_cast(0x80U >> (shifted & 7)); + if (detail::isInk(rgb565, conversion)) { + row[shifted >> 3] |= mask; + } else { + row[shifted >> 3] &= static_cast(~mask); + } + break; + } + } +} + +uint16_t Surface::readPixel(int x, int local_y) const { + const uint8_t* row = data_ + static_cast(local_y) * stride_; + switch (format_) { + case DISPLAY_COLOR_FORMAT_RGB565: + return reinterpret_cast(row)[x]; + case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: + return detail::byteswap16(reinterpret_cast(row)[x]); + case DISPLAY_COLOR_FORMAT_BGR565: + return detail::swapRedBlue(reinterpret_cast(row)[x]); + case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: + return detail::swapRedBlue(detail::byteswap16(reinterpret_cast(row)[x])); + case DISPLAY_COLOR_FORMAT_RGB888: { + const uint8_t rgb[3] = { row[x * 3], row[x * 3 + 1], row[x * 3 + 2] }; + return detail::rgb888To565(rgb); + } + case DISPLAY_COLOR_FORMAT_GRAYSCALE8: { + const uint8_t gray = row[x]; + const uint8_t rgb[3] = { gray, gray, gray }; + return detail::rgb888To565(rgb); + } + case DISPLAY_COLOR_FORMAT_MONOCHROME: + default: { + const int shifted = x + x_offset_bits_; + const uint8_t mask = static_cast(0x80U >> (shifted & 7)); + return (row[shifted >> 3] & mask) ? 0xFFFF : 0x0000; + } + } +} + +void Surface::markDirtyRect(int x0, int y0, int x1, int y1) { + if (dirty_ == nullptr) { + return; + } + + x0 = std::max(x0, clip_.x0); + x1 = std::min(x1, clip_.x1); + y0 = std::max(y0, clip_.y0); + y1 = std::min(y1, clip_.y1); + if (x0 >= x1 || y0 >= y1) { + return; + } + + dirty_->add(Rect { x0, y0, x1, y1 }); +} + +void Surface::markDirty(int x, int y, int width, int height) { + markDirtyRect(x, y, x + width, y + height); +} + +void Surface::plotRaw(int x, int y, uint16_t rgb565) { + if (!clip_.contains(x, y)) { + return; + } + writePixel(x - bounds_.x0, y - bounds_.y0, rgb565); +} + +void Surface::fillSpanRaw(int x0, int x1, int y, uint16_t rgb565) { + x0 = std::max(x0, clip_.x0); + x1 = std::min(x1, clip_.x1); + if (x0 >= x1 || y < clip_.y0 || y >= clip_.y1) { + return; + } + + const int local_x0 = x0 - bounds_.x0; + const int local_x1 = x1 - bounds_.x0; + const int local_y = y - bounds_.y0; + uint8_t* row = data_ + static_cast(local_y) * stride_; + + if (isFastRgb565()) { + const uint16_t native = toNative565(rgb565); + auto* pixels = reinterpret_cast(row); + std::fill(pixels + local_x0, pixels + local_x1, native); + return; + } + + if (format_ == DISPLAY_COLOR_FORMAT_GRAYSCALE8) { + std::fill(row + local_x0, row + local_x1, detail::toLuma(rgb565)); + return; + } + + for (int x = local_x0; x < local_x1; x++) { + writePixel(x, local_y, rgb565); + } +} + +void Surface::fillRectRaw(const Rect& rect, uint16_t rgb565) { + const Rect clipped = rect.intersect(clip_); + if (clipped.empty()) { + return; + } + + for (int y = clipped.y0; y < clipped.y1; y++) { + fillSpanRaw(clipped.x0, clipped.x1, y, rgb565); + } +} + +void Surface::fillRect(int x, int y, int width, int height, uint16_t rgb565) { + if (width <= 0 || height <= 0) { + return; + } + + const Rect clipped = Rect { x, y, x + width, y + height }.intersect(clip_); + if (clipped.empty()) { + return; + } + + fillRectRaw(clipped, rgb565); + markDirtyRect(clipped.x0, clipped.y0, clipped.x1, clipped.y1); +} + +void Surface::hline(int x, int y, int length, uint16_t rgb565) { + fillRect(x, y, length, 1, rgb565); +} + +void Surface::vline(int x, int y, int length, uint16_t rgb565) { + fillRect(x, y, 1, length, rgb565); +} + +void Surface::pixel(int x, int y, uint16_t rgb565) { + if (!clip_.contains(x, y)) { + return; + } + writePixel(x - bounds_.x0, y - bounds_.y0, rgb565); + markDirtyRect(x, y, x + 1, y + 1); +} + +void Surface::clear(uint16_t rgb565) { + fillRect(bounds_, rgb565); +} + +void Surface::lineRaw(int x0, int y0, int x1, int y1, uint16_t rgb565) { + const int dx = std::abs(x1 - x0); + const int sx = x0 < x1 ? 1 : -1; + const int dy = -std::abs(y1 - y0); + const int sy = y0 < y1 ? 1 : -1; + int error = dx + dy; + + while (true) { + plotRaw(x0, y0, rgb565); + if (x0 == x1 && y0 == y1) { + break; + } + + const int e2 = 2 * error; + if (e2 >= dy) { + error += dy; + x0 += sx; + } + if (e2 <= dx) { + error += dx; + y0 += sy; + } + } +} + +void Surface::line(int x0, int y0, int x1, int y1, uint16_t rgb565) { + const Rect box = Rect { + std::min(x0, x1), + std::min(y0, y1), + std::max(x0, x1) + 1, + std::max(y0, y1) + 1, + }.intersect(clip_); + if (box.empty()) { + return; + } + + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + lineRaw(x0, y0, x1, y1, rgb565); +} + +void Surface::drawRect(int x, int y, int width, int height, uint16_t rgb565) { + if (width <= 0 || height <= 0) { + return; + } + + const Rect box = Rect { x, y, x + width, y + height }.intersect(clip_); + if (box.empty()) { + return; + } + + markDirtyRect(box.x0, box.y0, box.x1, box.y1); + + fillSpanRaw(x, x + width, y, rgb565); + if (height > 1) { + fillSpanRaw(x, x + width, y + height - 1, rgb565); + } + if (height > 2) { + fillRectRaw(Rect { x, y + 1, x + 1, y + height - 1 }, rgb565); + fillRectRaw(Rect { x + width - 1, y + 1, x + width, y + height - 1 }, rgb565); + } +} + +void Surface::gradient(const Rect& rect, uint16_t from, uint16_t to, bool horizontal) { + const Rect area = rect.intersect(clip_); + if (area.empty()) { + return; + } + + markDirtyRect(area.x0, area.y0, area.x1, area.y1); + + if (horizontal) { + const int width = rect.width(); + for (int x = area.x0; x < area.x1; x++) { + const int amount = width <= 1 ? 0 : (x - rect.x0) * 255 / (width - 1); + fillRectRaw(Rect { x, area.y0, x + 1, area.y1 }, detail::blend565(from, to, static_cast(amount))); + } + return; + } + + const int height = rect.height(); + for (int y = area.y0; y < area.y1; y++) { + const int amount = height <= 1 ? 0 : (y - rect.y0) * 255 / (height - 1); + fillSpanRaw(area.x0, area.x1, y, detail::blend565(from, to, static_cast(amount))); + } +} + +} // namespace gfx diff --git a/Libraries/Gfx/Source/Target.cpp b/Libraries/Gfx/Source/Target.cpp new file mode 100644 index 0000000..faab44e --- /dev/null +++ b/Libraries/Gfx/Source/Target.cpp @@ -0,0 +1,688 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include + +#ifdef ESP_PLATFORM +#include +#include +#include +#endif + +#if defined(ESP_PLATFORM) && defined(SOC_PPA_SUPPORTED) && SOC_PPA_SUPPORTED +#include +#define GFX_HAS_PPA 1 +#else +#define GFX_HAS_PPA 0 +#endif + +#include +#include +#include + +namespace gfx { + +namespace { + +constexpr auto* TAG = "gfx"; + +/** Scratch space for one band. Small enough for an ESP32 without PSRAM, big enough for few bands. */ +constexpr std::size_t BAND_BUDGET_BYTES = 32 * 1024; + +/** Only use a full-frame buffer when it leaves at least this much room for the rest of the app. */ +constexpr std::size_t FULL_FRAME_HEADROOM = 2; + +#ifdef ESP_PLATFORM +int64_t nowUs() { + return esp_timer_get_time(); +} +#else +int64_t nowUs() { + return 0; +} +#endif + +} // namespace + +Target::Target(struct Device* display) : display_(display) { + if (display_ == nullptr) { + return; + } + + if (device_get(display_) != ERROR_NONE) { + display_ = nullptr; + return; + } + + if (!setup()) { + LOG_E(TAG, "Display can't be used for direct rendering"); + } +} + +Target::~Target() { + release(); + if (display_ != nullptr) { + device_put(display_); + } +} + +void Target::release() { + if (flip_buffers_[0] != nullptr) { + pixel_buffer_free(flip_buffers_[0]); + flip_buffers_[0] = nullptr; + } + if (flip_buffers_[1] != nullptr) { + pixel_buffer_free(flip_buffers_[1]); + flip_buffers_[1] = nullptr; + } + if (frame_buffer_ != nullptr) { + pixel_buffer_free(frame_buffer_); + frame_buffer_ = nullptr; + } + if (band_buffer_ != nullptr) { + pixel_buffer_free(band_buffer_); + band_buffer_ = nullptr; + } + if (staging_ != nullptr) { + memory_free(staging_); + staging_ = nullptr; + staging_capacity_ = 0; + } +#if GFX_HAS_PPA + if (ppa_client_ != nullptr) { + ppa_unregister_client(static_cast(ppa_client_)); + ppa_client_ = nullptr; + } +#endif + if (source_buffer_ != nullptr) { + pixel_buffer_free(source_buffer_); + source_buffer_ = nullptr; + } + if (output_buffer_ != nullptr) { + pixel_buffer_free(output_buffer_); + output_buffer_ = nullptr; + } + + using_hw_output_ = false; + back_index_ = 0; + present_full_ = false; + transform_ = {}; + dirty_.clear(); + previous_dirty_.clear(); + flip_has_previous_ = false; + first_frame_ = true; + info_.mode = RenderMode::None; + info_.hardwareFrameBuffer = false; +} + +bool Target::setup() { + info_.width = display_get_resolution_x(display_); + info_.height = display_get_resolution_y(display_); + info_.format = display_get_color_format(display_); + info_.slowRefresh = display_has_capability(display_, DISPLAY_CAPABILITY_SLOW_REFRESH); + info_.fullFrameRequired = info_.format == DISPLAY_COLOR_FORMAT_MONOCHROME + || display_has_capability(display_, DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME); + info_.bandHeight = info_.height; + + if (info_.width <= 0 || info_.height <= 0) { + return false; + } + + info_.frameWidth = info_.width; + info_.frameHeight = info_.height; + + if (display_get_frame_buffer_count(display_) >= 2 && setupFlip()) { + return true; + } + + if (info_.fullFrameRequired) { + return setupFullFrame(true); + } + + if (setupFullFrame(false)) { + return true; + } + + return setupBanded(); +} + +bool Target::setupFlip() { + void* first = nullptr; + void* second = nullptr; + display_get_frame_buffer(display_, 0, &first); + display_get_frame_buffer(display_, 1, &second); + if (first == nullptr || second == nullptr) { + return false; + } + + flip_buffers_[0] = pixel_buffer_wrap(first, info_.format, info_.width, info_.height, 0); + flip_buffers_[1] = pixel_buffer_wrap(second, info_.format, info_.width, info_.height, 0); + if (flip_buffers_[0] == nullptr || flip_buffers_[1] == nullptr) { + return false; + } + + // The buffers hold whatever the previous owner left behind, and a callback only clears the + // regions it draws into, so start both from black. + for (auto* buffer : flip_buffers_) { + pixel_buffer_clear(buffer); + pixel_buffer_msync(buffer, 0, 0, info_.width, info_.height); + } + + back_index_ = 0; + info_.hardwareFrameBuffer = true; + info_.mode = RenderMode::Flip; + LOG_I(TAG, "Rendering %dx%d by flipping hardware frame buffers", info_.width, info_.height); + return true; +} + +bool Target::setupFullFrame(bool required) { + const std::size_t stride = pixel_buffer_row_stride_bytes(info_.format, info_.width); + const std::size_t frame_bytes = stride * static_cast(info_.height); + const bool display_copies = display_has_capability(display_, DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM); + +#ifdef ESP_PLATFORM + if (!required) { + // malloc-cap failures are the real test, but this keeps a frame from eating the whole heap + const uint32_t caps = display_copies ? MALLOC_CAP_DEFAULT : MALLOC_CAP_DMA; + if (frame_bytes * FULL_FRAME_HEADROOM > heap_caps_get_largest_free_block(caps)) { + return false; + } + } +#else + (void)required; + (void)frame_bytes; +#endif + + // DMA-capable unless the driver copies out of the buffer, since panel drivers read it directly + const MemoryPolicy policy = display_copies + ? MemoryPolicy { 0, MEMORY_CAPABILITY_EXTERNAL, 64 } + : MemoryPolicy { MEMORY_CAPABILITY_DMA, MEMORY_CAPABILITY_EXTERNAL, 64 }; + + frame_buffer_ = pixel_buffer_create_with_policy(info_.format, info_.width, info_.height, &policy); + if (frame_buffer_ == nullptr) { + return false; + } + + // Dirty rectangles are presented tightly packed (see presentDirty()), which needs a staging + // buffer. Small panels' drivers copy the data anyway, so this is only CPU work there; for + // panels that drive only the region they're given, a tight rectangle is much cheaper than + // whole rows. + if (!info_.fullFrameRequired) { + staging_capacity_ = BAND_BUDGET_BYTES; + const MemoryPolicy staging_policy { MEMORY_CAPABILITY_INTERNAL | MEMORY_CAPABILITY_DMA, 0, 64 }; + staging_ = static_cast(memory_alloc_with_policy(staging_capacity_, &staging_policy)); + if (staging_ == nullptr) { + staging_capacity_ = 0; + } + } + + present_full_ = info_.fullFrameRequired; + info_.mode = RenderMode::FullFrame; + LOG_I(TAG, "Rendering %dx%d from a full-frame buffer", info_.width, info_.height); + return true; +} + +bool Target::setupBanded() { + const std::size_t stride = pixel_buffer_row_stride_bytes(info_.format, info_.width); + if (stride == 0) { + return false; + } + + int rows = static_cast(BAND_BUDGET_BYTES / stride); + rows = std::max(rows, 1); + rows = std::min(rows, info_.height); + + // Spread the rows over the bands evenly, so the last band isn't a sliver + const int bands = (info_.height + rows - 1) / rows; + rows = (info_.height + bands - 1) / bands; + + // Bands are small and go straight to DMA, so keep them in internal memory: on ESP32-class + // chips PSRAM can't be used for SPI DMA at all. + const MemoryPolicy policy { MEMORY_CAPABILITY_INTERNAL | MEMORY_CAPABILITY_DMA, 0, 64 }; + band_buffer_ = pixel_buffer_create_with_policy(info_.format, info_.width, rows, &policy); + if (band_buffer_ == nullptr) { + // Last resort: some chips can DMA from external memory and have little internal RAM free + const MemoryPolicy external { MEMORY_CAPABILITY_DMA, MEMORY_CAPABILITY_EXTERNAL, 64 }; + band_buffer_ = pixel_buffer_create_with_policy(info_.format, info_.width, rows, &external); + } + if (band_buffer_ == nullptr) { + return false; + } + + info_.bandHeight = rows; + info_.mode = RenderMode::Banded; + LOG_I(TAG, "Rendering %dx%d in %d bands of %d rows", info_.width, info_.height, bands, rows); + return true; +} + +Rect Target::clampRect(const Rect& rect) const { + Rect result = rect; + result.x0 = std::max(result.x0, 0); + result.y0 = std::max(result.y0, 0); + result.x1 = std::min(result.x1, info_.width); + result.y1 = std::min(result.y1, info_.height); + return result; +} + +void Target::copyRect(struct PixelBuffer* dst, struct PixelBuffer* src, const Rect& rect) { + if (info_.format == DISPLAY_COLOR_FORMAT_MONOCHROME) { + // Bit-packed rows: let the module deal with the packing + pixel_buffer_blit( + dst, rect.x0, rect.y0, + src, rect.x0, rect.y0, + rect.width(), rect.height(), + PIXEL_BUFFER_CONVERSION_EXACT_BLACK + ); + return; + } + + const std::size_t bytes_per_pixel = pixel_buffer_row_stride_bytes(info_.format, 1); + const std::size_t stride = pixel_buffer_get_stride_bytes(src); + const auto* src_data = static_cast(pixel_buffer_get_data(src)); + auto* dst_data = static_cast(pixel_buffer_get_data(dst)); + + const std::size_t row_bytes = static_cast(rect.width()) * bytes_per_pixel; + const std::size_t x_offset = static_cast(rect.x0) * bytes_per_pixel; + const std::size_t y_offset = static_cast(rect.y0) * stride; + + if (rect.x0 == 0 && rect.x1 == info_.width) { + // Full-width: one memcpy instead of one per row + memcpy(dst_data + y_offset, src_data + y_offset, row_bytes * static_cast(rect.height())); + return; + } + + for (int row = 0; row < rect.height(); row++) { + const std::size_t offset = y_offset + static_cast(row) * stride + x_offset; + memcpy(dst_data + offset, src_data + offset, row_bytes); + } +} + +void Target::updateStats(bool whole_frame) { + if (whole_frame) { + stats_.dirtyRects = 1; + stats_.dirtyPixels = info_.width * info_.height; + return; + } + + stats_.dirtyRects = 0; + stats_.dirtyPixels = 0; + for (int i = 0; i < dirty_.count(); i++) { + const Rect rect = clampRect(dirty_.at(i)); + if (rect.empty()) { + continue; + } + stats_.dirtyRects++; + stats_.dirtyPixels += rect.width() * rect.height(); + } +} + +error_t Target::refresh() { + if (!valid()) { + return ERROR_INVALID_STATE; + } + return display_refresh(display_); +} + +void Target::presentDirty(struct PixelBuffer* buffer) { + const std::size_t bytes_per_pixel = pixel_buffer_row_stride_bytes(info_.format, 1); + const std::size_t stride = pixel_buffer_get_stride_bytes(buffer); + const auto* data = static_cast(pixel_buffer_get_data(buffer)); + + for (int i = 0; i < dirty_.count(); i++) { + const Rect rect = clampRect(dirty_.at(i)); + if (rect.empty()) { + continue; + } + + const std::size_t row_bytes = static_cast(rect.width()) * bytes_per_pixel; + + if (staging_ == nullptr || row_bytes > staging_capacity_) { + // No staging buffer: present whole rows. Drivers assume the pixels for the rectangle + // are tightly packed, so a column range of a full-width buffer can't be passed on. + pixel_buffer_msync(buffer, 0, rect.y0, info_.width, rect.height()); + display_draw_bitmap(display_, 0, rect.y0, info_.width, rect.y1, data + static_cast(rect.y0) * stride); + continue; + } + + const std::size_t x_offset = static_cast(rect.x0) * bytes_per_pixel; + const int rows_per_chunk = static_cast(std::max(staging_capacity_ / row_bytes, 1)); + + for (int y = rect.y0; y < rect.y1; y += rows_per_chunk) { + const int rows = std::min(rows_per_chunk, rect.y1 - y); + + // Pack the rows tightly, because draw_bitmap() has no stride parameter + for (int row = 0; row < rows; row++) { + memcpy( + staging_ + static_cast(row) * row_bytes, + data + static_cast(y + row) * stride + x_offset, + row_bytes + ); + } + + display_draw_bitmap(display_, rect.x0, y, rect.x1, y + rows, staging_); + } + } +} + +bool Target::set_transform(const Transform& transform) { + if (!valid() || info_.mode == RenderMode::Transform) { + return false; + } + if (transform.width <= 0 || transform.height <= 0) { + return false; + } + + const int rotation = ((transform.rotation % 360) + 360) % 360; + if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) { + return false; + } + + // Asking for the native size and orientation means there is nothing to transform + if (rotation == 0 && transform.width == info_.width && transform.height == info_.height) { + info_.frameWidth = transform.width; + info_.frameHeight = transform.height; + return true; + } + +#if GFX_HAS_PPA + if (info_.format != DISPLAY_COLOR_FORMAT_RGB565) { + LOG_W(TAG, "Rotation/scaling needs an RGB565 display"); + return false; + } + + const MemoryPolicy source_policy { 0, MEMORY_CAPABILITY_EXTERNAL, 64 }; + source_buffer_ = pixel_buffer_create_with_policy(info_.format, transform.width, transform.height, &source_policy); + if (source_buffer_ == nullptr) { + return false; + } + + // Prefer writing the scaler's output straight into a panel frame buffer (no copy, no tearing); + // otherwise keep a native-size buffer and hand that to draw_bitmap. + if (flip_buffers_[0] == nullptr) { + output_buffer_ = pixel_buffer_create_with_policy(info_.format, info_.width, info_.height, &source_policy); + if (output_buffer_ == nullptr) { + pixel_buffer_free(source_buffer_); + source_buffer_ = nullptr; + return false; + } + } + + ppa_client_config_t client_config = { + .oper_type = PPA_OPERATION_SRM, + .max_pending_trans_num = 1, + .data_burst_length = PPA_DATA_BURST_LENGTH_128, + .flags = { .allow_pd = 0 }, + }; + ppa_client_handle_t client = nullptr; + if (ppa_register_client(&client_config, &client) != ESP_OK) { + LOG_E(TAG, "Failed to register the PPA client"); + if (source_buffer_ != nullptr) { + pixel_buffer_free(source_buffer_); + source_buffer_ = nullptr; + } + if (output_buffer_ != nullptr) { + pixel_buffer_free(output_buffer_); + output_buffer_ = nullptr; + } + return false; + } + ppa_client_ = client; + + transform_ = transform; + transform_.rotation = rotation; + using_hw_output_ = flip_buffers_[0] != nullptr; + back_index_ = 0; + info_.frameWidth = transform.width; + info_.frameHeight = transform.height; + info_.mode = RenderMode::Transform; + + LOG_I(TAG, "Rendering %dx%d rotated %d deg onto %dx%d", + transform_.width, transform_.height, transform_.rotation, info_.width, info_.height); + return true; +#else + LOG_W(TAG, "This target has no hardware rotation/scaling"); + return false; +#endif +} + +void Target::presentTransformed(struct PixelBuffer* output) { +#if GFX_HAS_PPA + const int32_t in_w = transform_.width; + const int32_t in_h = transform_.height; + const int32_t out_w = info_.width; + const int32_t out_h = info_.height; + + float scale_x; + float scale_y; + ppa_srm_rotation_angle_t angle = PPA_SRM_ROTATION_ANGLE_0; + + // The PPA scales first, then rotates. A quarter turn means the scaled width becomes the + // output's height (see Doom's 320x200 -> 720x1280 path for the same mapping). + switch (transform_.rotation) { + case 90: + scale_x = static_cast(out_h) / static_cast(in_w); + scale_y = static_cast(out_w) / static_cast(in_h); + angle = PPA_SRM_ROTATION_ANGLE_90; + break; + case 180: + scale_x = static_cast(out_w) / static_cast(in_w); + scale_y = static_cast(out_h) / static_cast(in_h); + angle = PPA_SRM_ROTATION_ANGLE_180; + break; + case 270: + scale_x = static_cast(out_h) / static_cast(in_w); + scale_y = static_cast(out_w) / static_cast(in_h); + angle = PPA_SRM_ROTATION_ANGLE_270; + break; + default: + scale_x = static_cast(out_w) / static_cast(in_w); + scale_y = static_cast(out_h) / static_cast(in_h); + break; + } + + ppa_srm_oper_config_t config = { + .in = { + .buffer = pixel_buffer_get_data(source_buffer_), + .pic_w = static_cast(in_w), + .pic_h = static_cast(in_h), + .block_w = static_cast(in_w), + .block_h = static_cast(in_h), + .block_offset_x = 0, + .block_offset_y = 0, + .srm_cm = PPA_SRM_COLOR_MODE_RGB565, + .yuv_range = PPA_COLOR_RANGE_LIMIT, + .yuv_std = PPA_COLOR_CONV_STD_RGB_YUV_BT601, + }, + .out = { + .buffer = pixel_buffer_get_data(output), + .buffer_size = static_cast(out_w * out_h * 2), + .pic_w = static_cast(out_w), + .pic_h = static_cast(out_h), + .block_offset_x = 0, + .block_offset_y = 0, + .srm_cm = PPA_SRM_COLOR_MODE_RGB565, + .yuv_range = PPA_COLOR_RANGE_LIMIT, + .yuv_std = PPA_COLOR_CONV_STD_RGB_YUV_BT601, + }, + .rotation_angle = angle, + .scale_x = scale_x, + .scale_y = scale_y, + .mirror_x = false, + .mirror_y = false, + .rgb_swap = false, + .byte_swap = false, + .alpha_update_mode = PPA_ALPHA_NO_CHANGE, + .alpha_fix_val = 0, + .mode = PPA_TRANS_MODE_BLOCKING, + .user_data = nullptr, + }; + + if (ppa_do_scale_rotate_mirror(static_cast(ppa_client_), &config) != ESP_OK) { + LOG_E(TAG, "PPA transfer failed"); + } +#else + (void)output; +#endif +} + +void Target::render(RenderCallback callback, void* context, Prepare prepare) { + if (!valid() || callback == nullptr) { + return; + } + + switch (info_.mode) { + case RenderMode::Flip: { + struct PixelBuffer* back = flip_buffers_[back_index_]; + struct PixelBuffer* front = flip_buffers_[1 - back_index_]; + const bool copy_previous = prepare == Prepare::Preserve && flip_has_previous_; + const int64_t t0 = nowUs(); + + // The back buffer holds the frame from two frames ago. Only the regions the previous + // frame touched differ between the two buffers, so copy just those forward instead of + // the whole frame - on a 1280x720 panel that's the difference between ~2 MB and a + // couple of polygon-sized rectangles per frame. + if (copy_previous) { + for (int i = 0; i < previous_dirty_.count(); i++) { + const Rect rect = clampRect(previous_dirty_.at(i)); + if (rect.empty()) { + continue; + } + copyRect(back, front, rect); + } + } + const int64_t t1 = nowUs(); + + dirty_.clear(); + Surface surface(back, 0, info_.height, &dirty_); + callback(surface, context); + const int64_t t2 = nowUs(); + + // Write back both the copied regions and the ones drawn this frame. Sync whole rows: + // pixel_buffer_msync() only takes its one-call contiguous path for full-width ranges, + // and a sub-rectangle would cost one esp_cache_msync() per row. + if (copy_previous) { + for (int i = 0; i < previous_dirty_.count(); i++) { + const Rect rect = clampRect(previous_dirty_.at(i)); + if (!rect.empty()) { + pixel_buffer_msync(back, 0, rect.y0, info_.width, rect.height()); + } + } + } + for (int i = 0; i < dirty_.count(); i++) { + const Rect rect = clampRect(dirty_.at(i)); + if (!rect.empty()) { + pixel_buffer_msync(back, 0, rect.y0, info_.width, rect.height()); + } + } + const int64_t t3 = nowUs(); + + display_draw_bitmap(display_, 0, 0, info_.width, info_.height, pixel_buffer_get_data(back)); + const int64_t t4 = nowUs(); + + back_index_ = 1 - back_index_; + previous_dirty_ = dirty_; + flip_has_previous_ = true; + updateStats(false); stats_.copyUs = static_cast(t1 - t0); + stats_.drawUs = static_cast(t2 - t1); + stats_.syncUs = static_cast(t3 - t2); + stats_.presentUs = static_cast(t4 - t3); + stats_.totalUs = static_cast(t4 - t0); + break; + } + + case RenderMode::FullFrame: { + dirty_.clear(); + + const int64_t t0 = nowUs(); + Surface surface(frame_buffer_, 0, info_.height, &dirty_); + callback(surface, context); + const int64_t t1 = nowUs(); + + // The first frame always goes out whole: the panel still shows whatever the previous + // owner left, and the buffer starts black, so a dirty-region present would leave that + // content on screen. + const bool whole_frame = present_full_ || first_frame_; + if (whole_frame) { + pixel_buffer_msync(frame_buffer_, 0, 0, info_.width, info_.height); + display_draw_bitmap(display_, 0, 0, info_.width, info_.height, pixel_buffer_get_data(frame_buffer_)); + } else { + presentDirty(frame_buffer_); + } + first_frame_ = false; + const int64_t t2 = nowUs(); + + updateStats(whole_frame); + stats_.drawUs = static_cast(t1 - t0); + stats_.presentUs = static_cast(t2 - t1); + stats_.totalUs = static_cast(t2 - t0); + break; + } + + case RenderMode::Banded: { + int64_t draw_total = 0; + int64_t present_total = 0; + + for (int y = 0; y < info_.height; y += info_.bandHeight) { + const int rows = std::min(info_.bandHeight, info_.height - y); + + pixel_buffer_clear(band_buffer_); + dirty_.clear(); + + const int64_t t0 = nowUs(); + Surface surface(band_buffer_, y, rows, &dirty_); + callback(surface, context); + const int64_t t1 = nowUs(); + + // Whole band: draw_bitmap() has no stride, so a sub-rectangle of this buffer would + // read past the end of each row. The band is only a few rows, so this is cheap. + pixel_buffer_msync(band_buffer_, 0, 0, info_.width, rows); + display_draw_bitmap(display_, 0, y, info_.width, y + rows, pixel_buffer_get_data(band_buffer_)); + const int64_t t2 = nowUs(); + + draw_total += t1 - t0; + present_total += t2 - t1; + } + + updateStats(true); + stats_.drawUs = static_cast(draw_total); + stats_.presentUs = static_cast(present_total); + stats_.totalUs = static_cast(draw_total + present_total); + break; + } + + case RenderMode::Transform: { + struct PixelBuffer* output = using_hw_output_ ? flip_buffers_[back_index_] : output_buffer_; + + const int64_t t0 = nowUs(); + Surface surface(source_buffer_, 0, info_.frameHeight, nullptr); + callback(surface, context); + const int64_t t1 = nowUs(); + + pixel_buffer_msync(source_buffer_, 0, 0, info_.frameWidth, info_.frameHeight); + presentTransformed(output); + pixel_buffer_msync(output, 0, 0, info_.width, info_.height); + + display_draw_bitmap(display_, 0, 0, info_.width, info_.height, pixel_buffer_get_data(output)); + const int64_t t2 = nowUs(); + + if (using_hw_output_) { + back_index_ = 1 - back_index_; + } + + stats_.dirtyRects = 1; + stats_.dirtyPixels = info_.frameWidth * info_.frameHeight; + stats_.drawUs = static_cast(t1 - t0); + stats_.presentUs = static_cast(t2 - t1); + stats_.totalUs = static_cast(t2 - t0); + break; + } + + case RenderMode::None: + break; + } +} + +} // namespace gfx diff --git a/Libraries/Gfx/Source/Text.cpp b/Libraries/Gfx/Source/Text.cpp new file mode 100644 index 0000000..4d65fa1 --- /dev/null +++ b/Libraries/Gfx/Source/Text.cpp @@ -0,0 +1,446 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include + +namespace gfx { + +namespace { + +struct LineSlice { + const char* begin; + const char* end; + const char* next; +}; + +TextStyle normaliseStyle(const TextStyle& style) { + TextStyle result = style; + if (result.font == nullptr) { + result.font = &FONT_5X7; + } + if (result.scale == 0) { + result.scale = 1; + } + return result; +} + +int glyphAdvance(const Font& font, int scale) { + return static_cast(font.advance) * scale; +} + +/** Horizontal step for one character, including tabs. */ +int advanceOfChar(char c, const Font& font, int scale, uint8_t tabSize) { + if (c == '\r') { + return 0; + } + if (c == '\t') { + const int cells = tabSize == 0 ? 4 : tabSize; + return glyphAdvance(font, scale) * cells; + } + return static_cast(font.advanceFor(c)) * scale; +} + +/** Width of [begin, end) in pixels. */ +int lineWidth(const char* begin, const char* end, const Font& font, int scale, uint8_t tabSize = 4) { + int width = 0; + for (const char* p = begin; p < end; p++) { + width += advanceOfChar(*p, font, scale, tabSize); + } + return width; +} + +/** Splits at newlines only. */ +LineSlice sourceLine(const char* p) { + const char* begin = p; + while (*p != '\0' && *p != '\n') { + p++; + } + const char* end = p; + const char* next = (*p == '\n') ? p + 1 : p; + return { begin, end, next }; +} + +/** + * Returns the next line that fits in maxWidth, breaking at the last space when there is one. + * Always makes progress: a single glyph wider than maxWidth is placed on its own line. + */ +LineSlice nextWrappedLine(const char* p, int maxWidth, const Font& font, int scale, uint8_t tabSize) { + const char* begin = p; + const char* end = p; + const char* lastSpace = nullptr; + int width = 0; + + while (*p != '\0' && *p != '\n') { + if (*p == '\r') { + p++; + continue; + } + if (*p == ' ') { + lastSpace = p; + } + + const int step = advanceOfChar(*p, font, scale, tabSize); + if (maxWidth > 0 && width + step > maxWidth && p != begin) { + if (lastSpace != nullptr && lastSpace > begin) { + end = lastSpace; + p = lastSpace + 1; + } else { + end = p; + } + return { begin, end, p }; + } + + width += step; + p++; + end = p; + } + + if (*p == '\n') { + p++; + } + return { begin, end, p }; +} + +/** + * Returns the prefix of [begin, end) that fits in maxWidth, writing its pixel width to outWidth. + */ +const char* ellipsizeLine(const char* begin, const char* end, int maxWidth, const Font& font, int scale, uint8_t tabSize, int* outWidth) { + const char* result = begin; + int width = 0; + for (const char* p = begin; p < end; p++) { + const int step = advanceOfChar(*p, font, scale, tabSize); + if (width + step > maxWidth) { + break; + } + width += step; + result = p + 1; + } + *outWidth = width; + return result; +} + +bool glyphBit(const Font& font, const uint8_t* glyph, int row, int col) { + if (font.layout == FontLayout::Rows) { + const int bytesPerRow = (font.glyphWidth + 7) / 8; + return (glyph[row * bytesPerRow + col / 8] & (0x80U >> (col & 7))) != 0; + } + if (font.layout == FontLayout::RowsLsb) { + const int bytesPerRow = (font.glyphWidth + 7) / 8; + return (glyph[row * bytesPerRow + col / 8] & (1U << (col & 7))) != 0; + } + const int bytesPerColumn = (font.glyphHeight + 7) / 8; + return (glyph[col * bytesPerColumn + row / 8] & (1U << (row & 7))) != 0; +} + +int alignedX(int anchor, int width, int boxWidth, TextAlign align) { + switch (align) { + case TextAlign::Center: + return anchor + (boxWidth - width) / 2; + case TextAlign::Right: + return anchor + boxWidth - width; + case TextAlign::Left: + default: + return anchor; + } +} + +} // namespace + +int measureText(const char* text, const Font& font, uint8_t scale, uint8_t tabSize) { + if (text == nullptr || *text == '\0') { + return 0; + } + + const int magnitude = scale == 0 ? 1 : scale; + int widest = 0; + const char* p = text; + while (true) { + const LineSlice line = sourceLine(p); + widest = std::max(widest, lineWidth(line.begin, line.end, font, magnitude, tabSize)); + if (*line.next == '\0') { + break; + } + p = line.next; + } + return widest; +} + +int measureTextHeight(const char* text, const Font& font, uint8_t scale) { + if (text == nullptr) { + return 0; + } + + const int magnitude = scale == 0 ? 1 : scale; + int lines = 1; + for (const char* p = text; *p != '\0'; p++) { + // A trailing newline doesn't add an empty line that drawing would visit. + if (*p == '\n' && p[1] != '\0') { + lines++; + } + } + return lines * lineHeight(font, static_cast(magnitude)); +} + +void Surface::drawGlyph(int x, int y, char c, const Font& font, uint16_t foreground, uint16_t background, bool opaque, uint8_t scale) { + const int magnitude = scale == 0 ? 1 : scale; + char glyphChar = c; + const uint8_t* glyph = font.glyph(c); + if (glyph == nullptr) { + glyphChar = '?'; + glyph = font.glyph(glyphChar); + } + if (glyph == nullptr || font.glyphWidth == 0 || font.glyphHeight == 0) { + return; + } + + const int columns = font.widthFor(glyphChar); + const int advance = font.advanceFor(glyphChar); + if (opaque) { + fillRectRaw( + Rect { x, y, x + advance * magnitude, y + static_cast(font.lineHeight) * magnitude }, + background + ); + } + + for (int row = 0; row < font.glyphHeight; row++) { + int runStart = -1; + for (int col = 0; col <= columns; col++) { + const bool on = col < columns && glyphBit(font, glyph, row, col); + if (on) { + if (runStart < 0) { + runStart = col; + } + } else if (runStart >= 0) { + const int x0 = x + runStart * magnitude; + const int x1 = x + col * magnitude; + for (int sy = 0; sy < magnitude; sy++) { + fillSpanRaw(x0, x1, y + row * magnitude + sy, foreground); + } + runStart = -1; + } + } + } +} + +void Surface::drawTextLine(int x, int y, const char* begin, const char* end, const TextStyle& style) { + const Font& font = *style.font; + const int magnitude = style.scale == 0 ? 1 : style.scale; + int cursor = x; + + for (const char* p = begin; p < end; p++) { + const char c = *p; + if (c == '\r') { + continue; + } + if (c == '\t') { + cursor += advanceOfChar(c, font, magnitude, style.tabSize); + continue; + } + if (static_cast(c) < 32) { + cursor += advanceOfChar(c, font, magnitude, style.tabSize); + continue; + } + drawGlyph(cursor, y, c, font, style.foreground, style.background, style.opaque, static_cast(magnitude)); + cursor += advanceOfChar(c, font, magnitude, style.tabSize); + } +} + +void Surface::drawText(int x, int y, const char* text, const Font& font, uint16_t foreground) { + TextStyle style; + style.font = &font; + style.foreground = foreground; + drawText(x, y, text, style); +} + +void Surface::drawText(int x, int y, const char* text, const Font& font, uint16_t foreground, uint16_t background) { + TextStyle style; + style.font = &font; + style.foreground = foreground; + style.background = background; + style.opaque = true; + drawText(x, y, text, style); +} + +void Surface::drawText(int x, int y, const char* text, const Font& font, uint16_t foreground, uint16_t background, uint8_t scale) { + TextStyle style; + style.font = &font; + style.foreground = foreground; + style.background = background; + style.opaque = true; + style.scale = scale; + drawText(x, y, text, style); +} + +void Surface::drawText(int x, int y, const char* text, const TextStyle& style) { + if (text == nullptr || *text == '\0') { + return; + } + + const TextStyle resolved = normaliseStyle(style); + const Font& font = *resolved.font; + const int magnitude = resolved.scale; + const int lineHeightPixels = static_cast(font.lineHeight) * magnitude; + + int lines = 0; + int widest = 0; + const char* p = text; + while (true) { + const LineSlice line = sourceLine(p); + widest = std::max(widest, lineWidth(line.begin, line.end, font, magnitude, resolved.tabSize)); + lines++; + if (*line.next == '\0') { + break; + } + p = line.next; + } + + const int blockHeight = lines * lineHeightPixels; + int startY = y; + if (resolved.verticalAlign == TextVerticalAlign::Middle) { + startY = y - blockHeight / 2; + } else if (resolved.verticalAlign == TextVerticalAlign::Bottom) { + startY = y - blockHeight; + } + + int left = x; + int right = x + widest; + if (resolved.align == TextAlign::Center) { + left = x - widest / 2; + right = left + widest; + } else if (resolved.align == TextAlign::Right) { + left = x - widest; + right = x; + } + markDirtyRect(left, startY, right, startY + blockHeight); + + p = text; + int lineY = startY; + while (true) { + const LineSlice line = sourceLine(p); + const int width = lineWidth(line.begin, line.end, font, magnitude, resolved.tabSize); + int lineX = x; + if (resolved.align == TextAlign::Center) { + lineX = x - width / 2; + } else if (resolved.align == TextAlign::Right) { + lineX = x - width; + } + drawTextLine(lineX, lineY, line.begin, line.end, resolved); + + if (*line.next == '\0') { + break; + } + p = line.next; + lineY += lineHeightPixels; + } +} + +void Surface::drawText(const Rect& box, const char* text, const TextStyle& style) { + if (text == nullptr || *text == '\0' || box.empty()) { + return; + } + + const TextStyle resolved = normaliseStyle(style); + const Font& font = *resolved.font; + const int magnitude = resolved.scale; + const int lineHeightPixels = static_cast(font.lineHeight) * magnitude; + + int lines = 0; + int widest = 0; + if (resolved.wrap == TextWrap::Word) { + const char* p = text; + while (true) { + const LineSlice line = nextWrappedLine(p, box.width(), font, magnitude, resolved.tabSize); + widest = std::max(widest, lineWidth(line.begin, line.end, font, magnitude, resolved.tabSize)); + lines++; + if (*line.next == '\0') { + break; + } + p = line.next; + } + } else { + const char* p = text; + while (true) { + const LineSlice line = sourceLine(p); + widest = std::max(widest, lineWidth(line.begin, line.end, font, magnitude, resolved.tabSize)); + lines++; + if (*line.next == '\0') { + break; + } + p = line.next; + } + } + + const int blockHeight = lines * lineHeightPixels; + int startY = box.y0; + if (resolved.verticalAlign == TextVerticalAlign::Middle) { + startY = box.y0 + (box.height() - blockHeight) / 2; + } else if (resolved.verticalAlign == TextVerticalAlign::Bottom) { + startY = box.y0 + box.height() - blockHeight; + } + + const int left = alignedX(box.x0, widest, box.width(), resolved.align); + markDirtyRect( + std::max(left, box.x0), + std::max(startY, box.y0), + std::min(left + widest, box.x1), + std::min(startY + blockHeight, box.y1) + ); + + // Clip to the box, so text that is too wide or too tall cannot leak into neighbouring content. + const Rect previousClip = clip_; + setClip(box); + + int lineY = startY; + const char* const ellipsisText = "..."; + const int ellipsisWidth = resolved.ellipsis + ? measureText(ellipsisText, font, static_cast(magnitude), resolved.tabSize) + : 0; + + if (resolved.wrap == TextWrap::Word) { + const char* p = text; + while (true) { + const LineSlice line = nextWrappedLine(p, box.width(), font, magnitude, resolved.tabSize); + const int width = lineWidth(line.begin, line.end, font, magnitude, resolved.tabSize); + const int lineX = alignedX(box.x0, width, box.width(), resolved.align); + drawTextLine(lineX, lineY, line.begin, line.end, resolved); + if (*line.next == '\0') { + break; + } + p = line.next; + lineY += lineHeightPixels; + } + } else { + const char* p = text; + while (true) { + const LineSlice line = sourceLine(p); + const int fullWidth = lineWidth(line.begin, line.end, font, magnitude, resolved.tabSize); + const char* drawEnd = line.end; + int renderedWidth = fullWidth; + int prefixWidth = fullWidth; + + if (resolved.ellipsis && fullWidth > box.width()) { + const int budget = box.width() - ellipsisWidth; + drawEnd = ellipsizeLine(line.begin, line.end, budget, font, magnitude, resolved.tabSize, &prefixWidth); + renderedWidth = prefixWidth + ellipsisWidth; + } + + const int lineX = alignedX(box.x0, renderedWidth, box.width(), resolved.align); + drawTextLine(lineX, lineY, line.begin, drawEnd, resolved); + if (drawEnd != line.end) { + drawTextLine(lineX + prefixWidth, lineY, ellipsisText, ellipsisText + 3, resolved); + } + + if (*line.next == '\0') { + break; + } + p = line.next; + lineY += lineHeightPixels; + } + } + + clip_ = previousClip; +} + +} // namespace gfx diff --git a/Libraries/Input/Include/input/AnyInput.h b/Libraries/Input/Include/input/AnyInput.h new file mode 100644 index 0000000..9a50c86 --- /dev/null +++ b/Libraries/Input/Include/input/AnyInput.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include + +/** + * "Any input" detection, mirroring the boot screen: a touch press, a key press, or a mouse or + * trackball button press all count. + * + * A touch that is already in progress when this object is constructed is deliberately ignored, so + * the tap that launched the app doesn't immediately close it. Call activity() from a loop, or wait() + * to block until something happens. + */ +class AnyInput { +public: + /** Finds the input devices; takes a reference on each of them until destruction. */ + AnyInput(); + + ~AnyInput(); + + AnyInput(const AnyInput&) = delete; + AnyInput& operator=(const AnyInput&) = delete; + + /** True when at least one input device (touch, keyboard, mouse, trackball) is available. */ + bool available() const; + + /** Non-blocking: true when a fresh press happened since the previous call. */ + bool activity(); + + /** + * Blocks until activity() returns true. + * @param timeout_ms how long to wait, or UINT32_MAX to wait forever + * @return true when input happened, false on timeout + */ + bool wait(uint32_t timeout_ms = UINT32_MAX); + +private: + static constexpr int MAX_KEYBOARDS = 4; + + struct Device* touch_ = nullptr; + struct Device* trackball_ = nullptr; + struct Device* mouse_ = nullptr; + struct Device* keyboards_[MAX_KEYBOARDS] {}; + int keyboard_count_ = 0; + + // Held state starts "down" so a touch/button held while the app starts doesn't count + bool touch_down_ = true; + bool trackball_down_ = true; + bool mouse_down_ = true; +}; diff --git a/Libraries/Input/LICENSE.md b/Libraries/Input/LICENSE.md new file mode 100644 index 0000000..f5f4b8b --- /dev/null +++ b/Libraries/Input/LICENSE.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Libraries/Input/Source/AnyInput.cpp b/Libraries/Input/Source/AnyInput.cpp new file mode 100644 index 0000000..10b63b0 --- /dev/null +++ b/Libraries/Input/Source/AnyInput.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include + +#include + +namespace { + +constexpr uint32_t POLL_INTERVAL_MS = 20; + +struct KeyboardCollection { + struct Device* devices[4] {}; + int count = 0; +}; + +bool collectKeyboard(struct Device* device, void* context) { + auto* collection = static_cast(context); + if (collection->count >= 4) { + return false; + } + if (device_is_ready(device) && device_get(device) == ERROR_NONE) { + collection->devices[collection->count++] = device; + } + return true; +} + +} // namespace + +AnyInput::AnyInput() { + KeyboardCollection collection {}; + device_for_each_of_type(&KEYBOARD_TYPE, &collection, collectKeyboard); + keyboard_count_ = collection.count; + for (int i = 0; i < keyboard_count_; i++) { + keyboards_[i] = collection.devices[i]; + } + + if (device_get_first_active_by_type(&TOUCH_TYPE, &touch_) != ERROR_NONE) { + touch_ = nullptr; + } + if (device_get_first_active_by_type(&TRACKBALL_TYPE, &trackball_) != ERROR_NONE) { + trackball_ = nullptr; + } + if (device_get_first_active_by_type(&MOUSE_TYPE, &mouse_) != ERROR_NONE) { + mouse_ = nullptr; + } +} + +AnyInput::~AnyInput() { + for (int i = 0; i < keyboard_count_; i++) { + device_put(keyboards_[i]); + } + if (touch_ != nullptr) { + device_put(touch_); + } + if (trackball_ != nullptr) { + device_put(trackball_); + } + if (mouse_ != nullptr) { + device_put(mouse_); + } +} + +bool AnyInput::available() const { + return touch_ != nullptr || trackball_ != nullptr || mouse_ != nullptr || keyboard_count_ > 0; +} + +bool AnyInput::activity() { + bool active = false; + + if (touch_ != nullptr) { + const bool touched = touch_read_data(touch_, 0) == ERROR_NONE + && [this]() { + uint16_t x, y, strength; + uint8_t count = 0; + return touch_get_touched_points(touch_, &x, &y, &strength, &count, 1) && count > 0; + }(); + if (touched && !touch_down_) { + active = true; + } + touch_down_ = touched; + } + + for (int i = 0; i < keyboard_count_; i++) { + struct KeyboardKeyData data {}; + while (keyboard_read_key(keyboards_[i], &data) == ERROR_NONE && data.key != 0) { + if (data.pressed) { + active = true; + } + if (!data.continue_reading) { + break; + } + } + } + + if (trackball_ != nullptr) { + bool pressed = false; + if (trackball_get_button_pressed(trackball_, &pressed) == ERROR_NONE) { + if (pressed && !trackball_down_) { + active = true; + } + trackball_down_ = pressed; + } + } + + if (mouse_ != nullptr) { + bool pressed = false; + if (mouse_get_button_pressed(mouse_, 0, &pressed) == ERROR_NONE) { + if (pressed && !mouse_down_) { + active = true; + } + mouse_down_ = pressed; + } + } + + return active; +} + +bool AnyInput::wait(uint32_t timeout_ms) { + uint32_t waited_ms = 0; + + while (true) { + if (activity()) { + return true; + } + if (timeout_ms != UINT32_MAX && waited_ms >= timeout_ms) { + return false; + } + + tt::kernel::delayTicks(tt::kernel::millisToTicks(POLL_INTERVAL_MS)); + if (timeout_ms != UINT32_MAX) { + waited_ms += POLL_INTERVAL_MS; + } + } +} diff --git a/Libraries/M5UnitModules/Include/Unit8Encoder.h b/Libraries/M5UnitModules/Include/Unit8Encoder.h index d205219..e61c1e9 100644 --- a/Libraries/M5UnitModules/Include/Unit8Encoder.h +++ b/Libraries/M5UnitModules/Include/Unit8Encoder.h @@ -14,7 +14,7 @@ * 0x60 : uint8_t toggle switch state (0=off, 1=on) [R] * 0x70 + i*3 : [R, G, B] LED colour for encoder i (i = 0-7, 0x00RRGGBB) [R/W] * 0x88 : [R, G, B] LED colour for the toggle switch (LED8) [R/W] - * 0xF0 : uint8_t firmware version [R] + * 0xFE : uint8_t firmware version [R] * 0xFF : uint8_t I2C address (R/W) * * LED indices: 0-7 = encoder LEDs, 8 = switch LED (total 9 LEDs). @@ -87,4 +87,6 @@ class Unit8Encoder { static constexpr uint8_t REG_SWITCH = 0x60; static constexpr uint8_t REG_LED = 0x70; // encoder LEDs 0-7 (24 bytes) static constexpr uint8_t REG_SWITCH_LED = 0x88; // switch LED (3 bytes) + static constexpr uint8_t REG_VERSION = 0xFE; // firmware version [R] + static constexpr uint8_t REG_ADDRESS = 0xFF; // I2C address [R/W] }; diff --git a/Libraries/M5UnitModules/Include/UnitCardKB2.h b/Libraries/M5UnitModules/Include/UnitCardKB2.h index 9ae8535..226d985 100644 --- a/Libraries/M5UnitModules/Include/UnitCardKB2.h +++ b/Libraries/M5UnitModules/Include/UnitCardKB2.h @@ -6,6 +6,7 @@ * UART (115200-8N-1) - via GROVE port after Fn+Sym+2 mode switch * * I2C: plain 1-byte read returns ASCII of held key (0 = none). + * Write 0xF1 then read one byte for the firmware version (current = 0x02). * * UART frame: AA [DATA_LEN=0x03] [KEY_ID] [KEY_STATE] [checksum] * KEY_ID = row*11 + col (0-43, rows 0-3, cols 0-10) @@ -57,6 +58,9 @@ class UnitCardKB2 { // Call end() before switching modes (begin→beginUart or vice versa). Mode mode() const { return mode_; } + // Firmware version read from register 0xF1 in I2C mode (0 when unknown). + uint8_t firmwareVersion() const { return version_; } + // Returns the ASCII value of the currently/last pressed key, or 0 if none. // In UART mode: drains available bytes from the UART RX buffer, parses frames, // returns the ASCII of the most recently pressed key (releases are ignored). @@ -70,6 +74,7 @@ class UnitCardKB2 { // I2C Device* i2cDev_ = nullptr; uint8_t addr_ = DEFAULT_ADDR; + uint8_t version_ = 0; char cachedKey_ = 0; // UART @@ -92,4 +97,5 @@ class UnitCardKB2 { char readFromI2C(); char pollUart(); + bool readVersion(uint8_t& version); }; diff --git a/Libraries/M5UnitModules/Include/UnitCommon.h b/Libraries/M5UnitModules/Include/UnitCommon.h index 374d20e..6d44034 100644 --- a/Libraries/M5UnitModules/Include/UnitCommon.h +++ b/Libraries/M5UnitModules/Include/UnitCommon.h @@ -6,7 +6,7 @@ * populates tx_buffer after STOP, before the next START). Using a repeated-START * (the default i2c_controller_read_register) causes bus errors on every read. * - * Pattern: write(reg) → STOP → vTaskDelay(2ms) → read(data) + * Pattern: write(reg) → STOP → vTaskDelay(3ms) → read(data) */ #pragma once @@ -18,27 +18,145 @@ static constexpr uint32_t UNIT_I2C_TIMEOUT_MS = 20; static constexpr uint32_t UNIT_I2C_PROBE_TIMEOUT_MS = 10; // shorter timeout for presence checks -static constexpr uint32_t UNIT_STM32_READ_DELAY_MS = 2; // STM32 needs STOP + delay before read +static constexpr uint32_t UNIT_STM32_READ_DELAY_MS = 3; // STM32 needs STOP + delay before read static constexpr uint16_t UNIT_MAX_WRITE_PAYLOAD = 32; // max bytes in a single I2C write +// A single probe can fail while a slave is still finishing its previous +// transaction (the 8Encoder's STM32 re-arms its I2C listener in the STOP +// callback). Retry a few times, but accept the first ACK: requiring every +// attempt to ACK rejects healthy STM32 units. Callers still validate a hit +// with a real register access in begin(). +// +// Five attempts matter for the Scroll/ByteButton/Joystick2 units: their first +// version read after a PaHub channel change is often lost while the downstream +// STM32 re-arms. Retrying with a 2 ms gap avoids a false "not found" on an +// otherwise healthy channel. +static constexpr int UNIT_I2C_PROBE_ATTEMPTS = 5; +static constexpr uint32_t UNIT_I2C_PROBE_RETRY_DELAY_MS = 2; + +// Identity registers used by unitProbeOnce(). +static constexpr uint8_t UNIT_CARDKB2_VERSION_REG = 0xF1; // returns 0x02 on current firmware +static constexpr uint8_t UNIT_RFID2_VERSION_REG = 0x37; // MFRC522/WS1850S VersionReg + // I2C addresses of the unit modules this library can drive (excludes PaHub itself -// and UART-only units like MIDI). Used by PaHub channel probing instead of a full -// 0x08-0x77 bus scan: the ESP-IDF i2c_master driver's probe op can wedge the bus -// FSM after heavy NACK traffic, so scanning only known addresses avoids that. +// and UART-only units like MIDI). Used by the PaHub channel scanner instead of a +// full 0x08-0x77 bus scan: the ESP-IDF i2c_master probe op is unreliable behind a +// mux, so only known addresses are checked. Common display/input units are listed +// first so a scan that finds one can stop early. Every entry has an identity read +// (no write-only probe), which is what stops a floating mux channel from +// producing false ACKs. static constexpr uint8_t KNOWN_UNIT_ADDRS[] = { - 0x28, // UnitRfid2 + 0x41, // Unit8Encoder (the most common PaHub unit, so find it quickly) 0x3E, // UnitLcd 0x40, // UnitScroll - 0x41, // Unit8Encoder 0x47, // UnitByteButton - 0x5F, // UnitCardKB2 0x63, // UnitJoystick2 + 0x28, // UnitRfid2 + 0x5F, // UnitCardKB2 }; // Probe whether a device exists at the given address on the given I2C bus. +// +// Do NOT use i2c_controller_has_device_at_address() here. On ESP-IDF 6.1 its +// i2c_master_probe() path can report ESP_OK for an absent address when the bus +// FSM is left in a non-error state; behind a PaHub that turned every address on +// every channel into a false ACK. A real transaction gives a reliable NACK. +// +// The transaction is chosen so it is harmless and identifies the device: +// - PaHub: read the control register. +// - 8Encoder/Scroll/ByteButton/Joystick2: read firmware version register +// 0xFE and require 1-15. +// - LCD: read the four-byte ID (command 0x04); require 0x77 0x89. +// - CardKB2: read firmware version register 0xF1 and require 1-15. This also +// avoids consuming a queued key byte, which a plain read would do. +// - RFID2: read the MFRC522/WS1850S VersionReg (0x37) and require a known +// WS1850S or MFRC522-family value. +inline bool unitProbeOnce(Device* dev, uint8_t addr) { + uint8_t value = 0; + switch (addr) { + case 0x70: // UnitPaHub - read the control register + return i2c_controller_read(dev, addr, &value, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) == ERROR_NONE; + case 0x41: // Unit8Encoder + case 0x40: // UnitScroll + case 0x47: // UnitByteButton + case 0x63: { // UnitJoystick2 + // These STM32 units expose a firmware version at 0xFE. A + // write-only probe is not enough behind a floating PaHub channel: + // an absent device can look ACKed on every address, and a real + // version value is a much stronger identity check. + uint8_t reg = 0xFE; + if (i2c_controller_write(dev, addr, ®, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) != ERROR_NONE) { + return false; + } + vTaskDelay(pdMS_TO_TICKS(UNIT_STM32_READ_DELAY_MS)); + if (i2c_controller_read(dev, addr, &value, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) != ERROR_NONE) { + return false; + } + return value >= 1 && value <= 15; + } + case 0x3E: { // UnitLcd - read the 4-byte ID (0x77 0x89 ...) + uint8_t cmd = 0x04; + if (i2c_controller_write(dev, addr, &cmd, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) != ERROR_NONE) { + return false; + } + uint8_t id[4] = {}; + if (i2c_controller_read(dev, addr, id, sizeof(id), + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) != ERROR_NONE) { + return false; + } + return id[0] == 0x77 && id[1] == 0x89; + } + case 0x5F: { // UnitCardKB2 - read the firmware version register + uint8_t reg = UNIT_CARDKB2_VERSION_REG; + if (i2c_controller_write(dev, addr, ®, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) != ERROR_NONE) { + return false; + } + vTaskDelay(pdMS_TO_TICKS(UNIT_STM32_READ_DELAY_MS)); + if (i2c_controller_read(dev, addr, &value, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) != ERROR_NONE) { + return false; + } + return value >= 1 && value <= 15; + } + case 0x28: { // UnitRfid2 - read the MFRC522/WS1850S VersionReg + uint8_t reg = UNIT_RFID2_VERSION_REG; + if (i2c_controller_write(dev, addr, ®, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) != ERROR_NONE) { + return false; + } + vTaskDelay(pdMS_TO_TICKS(UNIT_STM32_READ_DELAY_MS)); + if (i2c_controller_read(dev, addr, &value, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) != ERROR_NONE) { + return false; + } + // 0x15 is the version reported by the WS1850S in the RFID2 Unit. + // 0x88/0x90/0x91/0x92/0x12 are MFRC522/PN512 family values. + return value == 0x15 || value == 0x88 || value == 0x90 || + value == 0x91 || value == 0x92 || value == 0x12; + } + default: { // STM32 register-based units: set the register pointer only + uint8_t reg = 0x00; + return i2c_controller_write(dev, addr, ®, 1, + pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) == ERROR_NONE; + } + } +} + inline bool unitProbe(Device* dev, uint8_t addr) { - return i2c_controller_has_device_at_address( - dev, addr, pdMS_TO_TICKS(UNIT_I2C_PROBE_TIMEOUT_MS)) == ERROR_NONE; + for (int i = 0; i < UNIT_I2C_PROBE_ATTEMPTS; i++) { + if (unitProbeOnce(dev, addr)) { + return true; + } + if (i + 1 < UNIT_I2C_PROBE_ATTEMPTS) { + vTaskDelay(pdMS_TO_TICKS(UNIT_I2C_PROBE_RETRY_DELAY_MS)); + } + } + return false; } // Write [reg, buf...] as a single transaction (STOP at end). @@ -59,7 +177,7 @@ inline bool unitWriteRegPtr(Device* dev, uint8_t addr, uint8_t reg) { pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE; } -// Read from a register: write pointer → STOP → 2ms delay → read. +// Read from a register: write pointer → STOP → 3ms delay → read. inline bool unitReadReg(Device* dev, uint8_t addr, uint8_t reg, uint8_t* buf, uint16_t len) { if (len > 0 && buf == nullptr) return false; diff --git a/Libraries/M5UnitModules/Include/UnitPaHub.h b/Libraries/M5UnitModules/Include/UnitPaHub.h index 5750f67..d583b5e 100644 --- a/Libraries/M5UnitModules/Include/UnitPaHub.h +++ b/Libraries/M5UnitModules/Include/UnitPaHub.h @@ -1,8 +1,8 @@ /* - * UnitPaHub.h - M5Stack PaHub v2 I2C multiplexer (TCA9548A, addr 0x70) + * UnitPaHub.h - M5Stack PaHub v2 I2C multiplexer (PCA9548AP, addr 0x70) * * 6-channel I2C mux. Selecting a channel routes the downstream bus to that - * port only. Selecting channel 255 (or calling deselect()) disables all ports. + * port only. Calling deselect() disables all ports. * * Usage: * UnitPaHub hub; @@ -38,6 +38,9 @@ class UnitPaHub { // Select a single channel (0-5). Returns false on error. bool select(uint8_t channel); + // Read the control register back. Returns false on I2C error. + bool readChannel(uint8_t& mask); + // Disable all channels. bool deselect(); diff --git a/Libraries/M5UnitModules/Include/UnitRfid2.h b/Libraries/M5UnitModules/Include/UnitRfid2.h index 6c878e7..ce74bef 100644 --- a/Libraries/M5UnitModules/Include/UnitRfid2.h +++ b/Libraries/M5UnitModules/Include/UnitRfid2.h @@ -26,6 +26,7 @@ * 0x2B TPrescalerReg - timer prescaler * 0x2C TReloadRegH - timer reload high * 0x2D TReloadRegL - timer reload low + * 0x37 VersionReg - chip version (WS1850S 0x15; MFRC522 0x88/0x90/0x91/0x92/0x12) * * Usage: * UnitRfid2 rfid; @@ -88,6 +89,9 @@ class UnitRfid2 { bool begin(Device* dev, uint8_t addr = DEFAULT_ADDR); bool isPresent() const { return dev_ != nullptr; } + // MFRC522/WS1850S VersionReg value (0 when unknown). + uint8_t firmwareVersion() const { return version_; } + // --------------------------------------------------------------------------- // Card detection & UID // --------------------------------------------------------------------------- @@ -175,8 +179,9 @@ class UnitRfid2 { void haltCard(); private: - Device* dev_ = nullptr; - uint8_t addr_ = DEFAULT_ADDR; + Device* dev_ = nullptr; + uint8_t addr_ = DEFAULT_ADDR; + uint8_t version_ = 0; // --------------------------------------------------------------------------- // Low-level register access @@ -194,6 +199,7 @@ class UnitRfid2 { bool softReset(); void antennaOn(); + bool readVersion(uint8_t& version); // --------------------------------------------------------------------------- // CRC & transceive @@ -253,6 +259,7 @@ class UnitRfid2 { static constexpr uint8_t REG_TPRESCALER = 0x2B; static constexpr uint8_t REG_TRELOAD_H = 0x2C; static constexpr uint8_t REG_TRELOAD_L = 0x2D; + static constexpr uint8_t REG_VERSION = 0x37; // VersionReg // --------------------------------------------------------------------------- // MFRC522 commands diff --git a/Libraries/M5UnitModules/README.md b/Libraries/M5UnitModules/README.md index 47a7296..45fc217 100644 --- a/Libraries/M5UnitModules/README.md +++ b/Libraries/M5UnitModules/README.md @@ -10,7 +10,7 @@ Drivers for M5Stack Unit peripheral modules, for use with [Tactility OS](https:/ | `UnitByteButton.h` | ByteButton - 8 buttons + RGB LEDs | I2C | 0x47 | | `UnitJoystick2.h` | Joystick2 - XY joystick + button + LED | I2C | 0x63 | | `UnitScroll.h` | Scroll - single encoder + button + LED | I2C | 0x40 | -| `UnitPaHub.h` | PaHub - TCA9548A 6-channel I2C mux | I2C | 0x70 | +| `UnitPaHub.h` | PaHub - PCA9548AP 6-channel I2C mux | I2C | 0x70 | | `UnitLcd.h` | LCD Unit - 135x240 IPS display (ESP32-PICO) | I2C | 0x3E | | `UnitDualButton.h` | Dual Button - 2 GPIO buttons | GPIO | - | | `UnitCardKB2.h` | CardKB2 - QWERTY keyboard | I2C | 0x5F | @@ -21,7 +21,7 @@ Drivers for M5Stack Unit peripheral modules, for use with [Tactility OS](https:/ ### Without CMake component -Source files are compiled directly into your app (same pattern as `TactilityCpp` and `SoundEngine`). In your app's `main/CMakeLists.txt`: +Source files are compiled directly into your app. In your app's `main/CMakeLists.txt`: ```cmake file(GLOB_RECURSE SOURCE_FILES @@ -32,7 +32,6 @@ file(GLOB_RECURSE SOURCE_FILES idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS - ../../../Libraries/TactilityCpp/Include ../../../Libraries/M5UnitModules/Include REQUIRES TactilitySDK ) @@ -63,7 +62,7 @@ if (bb.begin(i2c)) { ### Through PaHub (I2C multiplexer) -When multiple units share the same I2C address, or more than 6 units are needed, connect them via a PaHub. Select the channel before each operation: +When multiple units share the same I2C address, connect them via a PaHub. The PaHub exposes six downstream channels; units on the same channel must still have unique addresses. Select the channel before each operation: ```cpp #include @@ -150,7 +149,9 @@ The driver tracks Aa (caps lock / one-shot shift), Sym (symbol mode), and Fn int Most M5Stack Units use an STM32 microcontroller internally. These require a STOP condition between the register-pointer write and the data read (STM32 HAL populates the tx buffer after STOP, before the next START). Using a repeated-START causes bus errors. -`UnitCommon.h` provides `unitReadReg` which implements the required STOP + 2ms delay pattern automatically. All STM32-based drivers use this helper. +`UnitCommon.h` provides `unitReadReg` which implements the required STOP + 3ms delay pattern automatically. All STM32-based drivers use this helper. + +`unitProbe` retries up to five times and accepts the first successful transaction. It deliberately does not use the ESP-IDF `i2c_master_probe()` path: on ESP-IDF 6.1 that can report a false ACK for an absent address, and behind a PaHub it made every known address appear present on every channel. Instead, `unitProbe` reads the PaHub control register, the firmware-version register (`0xFE`, accepted range 1-15) for the 8Encoder/Scroll/ByteButton/Joystick2, the four-byte ID (`0x04` -> `0x77 0x89 ...`) for the LCD, CardKB2 version `0xF1` (1-15), and RFID2 `VersionReg` `0x37` (WS1850S `0x15` or an MFRC522-family value). A successful probe is still followed by a real register read in `begin()`: the 8Encoder checks version plus address register `0xFF`, the LCD checks its ID, and CardKB2/RFID2 re-read their version registers. This prevents a floating PaHub channel from latching a scan onto an empty port. The LCD Unit is an exception - it contains an ESP32-PICO and uses a command-based I2C protocol without the STOP+delay requirement. @@ -260,7 +261,7 @@ NTAG21x UID bytes are read-only at the hardware level and cannot be cloned. | `TestUnitJoystick2` | Joystick2 | XY position bar graphs + button state + LED colour picker | | `TestUnitLcd` | LCD Unit | Displays a colour gradient and version info on the unit's screen | | `TestUnitMidi` | MIDI / Synth | Channel + program picker; Note On/Off for middle C | -| `TestUnitPaHub` | PaHub | Per-channel I2C address scanner with periodic re-probe | +| `TestUnitPaHub` | PaHub | Per-channel I2C identity scanner (tap a channel to probe) | | `TestUnitRfid2` | RFID2 | Card detection with UID, type, SAK/ATQA display; Clear to re-scan | | `TestUnitScroll` | Scroll | Encoder delta counter + button state + LED colour | diff --git a/Libraries/M5UnitModules/Source/Unit8Encoder.cpp b/Libraries/M5UnitModules/Source/Unit8Encoder.cpp index 5201fae..0ece89f 100644 --- a/Libraries/M5UnitModules/Source/Unit8Encoder.cpp +++ b/Libraries/M5UnitModules/Source/Unit8Encoder.cpp @@ -10,24 +10,75 @@ static inline void packRgb(uint8_t* dst, uint32_t rgb) { } bool Unit8Encoder::begin(Device* dev, uint8_t addr) { + dev_ = nullptr; + addr_ = 0; if (!dev || !device_is_ready(dev)) return false; if (!unitProbe(dev, addr)) { - ESP_LOGW(TAG, "8Encoder not found at 0x%02X", addr); + ESP_LOGD(TAG, "8Encoder not found at 0x%02X", addr); + return false; + } + // A floating or marginal downstream bus (common behind a PaHub with no + // device on the selected channel) can make a write-only presence probe + // look successful for every address. Read the firmware version and the + // unit's own address register to confirm that this really is an 8Encoder + // before touching LEDs or counters. + uint8_t version = 0; + bool identityOk = false; + for (int attempt = 0; attempt < 3; attempt++) { + uint8_t address = 0; + version = 0; + bool versionOk = unitReadReg(dev, addr, REG_VERSION, &version, 1); + vTaskDelay(pdMS_TO_TICKS(1)); + bool addressOk = unitReadReg(dev, addr, REG_ADDRESS, &address, 1); + if (versionOk && addressOk && + version >= 1 && version <= 15 && address == addr) { + identityOk = true; + break; + } + if (attempt + 1 < 3) vTaskDelay(pdMS_TO_TICKS(1)); + } + if (!identityOk) { + // The presence probe succeeded, so this is not an empty channel: either + // the bus is marginal or a different device answered 0x41. Surface it + // at warning level instead of silently hiding it. + ESP_LOGW(TAG, "8Encoder identity check failed at 0x%02X", addr); return false; } dev_ = dev; addr_ = addr; + // The STM32 briefly stops listening between the presence probe and the + // first data write. Retry the init writes so a healthy unit isn't rejected + // just because it was still re-arming. + auto writeWithRetry = [&](uint8_t reg, const uint8_t* data, uint16_t len) { + for (int attempt = 0; attempt < 3; attempt++) { + if (unitWriteReg(dev_, addr_, reg, data, len)) return true; + if (attempt + 1 < 3) vTaskDelay(pdMS_TO_TICKS(1)); + } + return false; + }; // Turn encoder LEDs off uint8_t off[ENCODER_LED_COUNT * 3] = {}; - if (!unitWriteReg(dev_, addr_, REG_LED, off, sizeof(off))) + bool ledOk = writeWithRetry(REG_LED, off, sizeof(off)); + if (!ledOk) ESP_LOGW(TAG, "8Encoder LED init write failed at 0x%02X", addr_); // Turn switch LED off uint8_t offSw[3] = {}; - if (!unitWriteReg(dev_, addr_, REG_SWITCH_LED, offSw, 3)) + bool switchOk = writeWithRetry(REG_SWITCH_LED, offSw, 3); + if (!switchOk) ESP_LOGW(TAG, "8Encoder switch LED init write failed at 0x%02X", addr_); + // A device can ACK the presence probe but reject writes (for example a + // different unit at 0x41). If neither register accepts data, leave this + // channel for the scan to try the next one instead of latching onto a + // device that isn't an 8Encoder. + if (!ledOk && !switchOk) { + ESP_LOGW(TAG, "8Encoder not responding at 0x%02X", addr_); + dev_ = nullptr; + addr_ = 0; + return false; + } // Poison cache so first flushLeds() always sends memset(ledColor_, 0xFF, sizeof(ledColor_)); - ESP_LOGI(TAG, "8Encoder ready at 0x%02X", addr_); + ESP_LOGI(TAG, "8Encoder ready at 0x%02X (fw %u)", addr_, (unsigned)version); return true; } diff --git a/Libraries/M5UnitModules/Source/UnitCardKB2.cpp b/Libraries/M5UnitModules/Source/UnitCardKB2.cpp index cbd3469..d99a338 100644 --- a/Libraries/M5UnitModules/Source/UnitCardKB2.cpp +++ b/Libraries/M5UnitModules/Source/UnitCardKB2.cpp @@ -69,6 +69,16 @@ static char fnCombo(uint8_t id) { // I2C // --------------------------------------------------------------------------- +bool UnitCardKB2::readVersion(uint8_t& version) { + version = 0; + if (!i2cDev_) return false; + if (!unitReadReg(i2cDev_, addr_, UNIT_CARDKB2_VERSION_REG, &version, 1)) { + version = 0; + return false; + } + return version >= 1 && version <= 15; +} + bool UnitCardKB2::begin(Device* dev, uint8_t addr) { end(); if (!dev || !device_is_ready(dev)) return false; @@ -78,8 +88,27 @@ bool UnitCardKB2::begin(Device* dev, uint8_t addr) { } i2cDev_ = dev; addr_ = addr; - mode_ = Mode::I2C; - ESP_LOGI(TAG, "CardKB2 ready (I2C) at 0x%02X", addr_); + + // unitProbe() already read the version, but the STM32 may still be busy + // after the probe's STOP. Retry the identity read before declaring success. + uint8_t version = 0; + bool identityOk = false; + for (int attempt = 0; attempt < 3; attempt++) { + if (readVersion(version)) { + identityOk = true; + break; + } + vTaskDelay(pdMS_TO_TICKS(2)); + } + if (!identityOk) { + ESP_LOGW(TAG, "CardKB2 identity check failed at 0x%02X", addr_); + i2cDev_ = nullptr; + return false; + } + + version_ = version; + mode_ = Mode::I2C; + ESP_LOGI(TAG, "CardKB2 ready (I2C) at 0x%02X (fw %u)", addr_, (unsigned)version_); return true; } @@ -225,6 +254,7 @@ void UnitCardKB2::end() { uartDev_ = nullptr; } i2cDev_ = nullptr; + version_ = 0; cachedKey_ = 0; frameState_ = FrameState::WaitAA; capsLock_ = false; diff --git a/Libraries/M5UnitModules/Source/UnitLcd.cpp b/Libraries/M5UnitModules/Source/UnitLcd.cpp index 1c6afd7..d8e1cfb 100644 --- a/Libraries/M5UnitModules/Source/UnitLcd.cpp +++ b/Libraries/M5UnitModules/Source/UnitLcd.cpp @@ -8,6 +8,7 @@ static constexpr auto* TAG = "UnitLcd"; static constexpr uint8_t CMD_SET_BRIGHTNESS = 0x22; static constexpr uint8_t CMD_SET_ROTATION = 0x36; +static constexpr uint8_t CMD_READ_ID = 0x04; static constexpr uint8_t CMD_FILL_RECT = 0x6A; static constexpr uint8_t CMD_DRAW_PIXEL = 0x62; static constexpr uint8_t CMD_SET_COL_RANGE = 0x2A; @@ -29,22 +30,57 @@ bool UnitLcd::sendCmd(const uint8_t* data, uint16_t len) { // --------------------------------------------------------------------------- bool UnitLcd::begin(Device* dev, uint8_t addr) { + dev_ = nullptr; + addr_ = 0; if (!dev || !device_is_ready(dev)) return false; if (!unitProbe(dev, addr)) { - ESP_LOGW(TAG, "LCD unit not found at 0x%02X", addr); + ESP_LOGD(TAG, "LCD unit not found at 0x%02X", addr); + return false; + } + // CMD_READ_ID returns 0x77 0x89 followed by two version bytes. Unlike a + // write-only probe, this rejects the false ACKs seen on floating PaHub + // channels, so the scanner cannot latch onto an empty port. + bool identityOk = false; + for (int attempt = 0; attempt < 3; attempt++) { + uint8_t cmd = CMD_READ_ID; + uint8_t id[4] = {}; + if (i2c_controller_write(dev, addr, &cmd, 1, + pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE && + i2c_controller_read(dev, addr, id, sizeof(id), + pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE && + id[0] == 0x77 && id[1] == 0x89) { + identityOk = true; + break; + } + if (attempt + 1 < 3) vTaskDelay(pdMS_TO_TICKS(1)); + } + if (!identityOk) { + // The presence probe succeeded, so the channel is not empty. If the + // ID cannot be confirmed, report it rather than silently skipping. + ESP_LOGW(TAG, "LCD identity check failed at 0x%02X", addr); return false; } dev_ = dev; addr_ = addr; rotation_ = 0; + // The LCD controller can still be busy immediately after the presence + // probe (especially when it is behind a freshly switched PaHub channel). + // Retry the two init commands instead of failing on the first NACK. + auto sendWithRetry = [&](const uint8_t* cmd, uint16_t len) { + for (int attempt = 0; attempt < 3; attempt++) { + if (sendCmd(cmd, len)) return true; + if (attempt + 1 < 3) vTaskDelay(pdMS_TO_TICKS(1)); + } + return false; + }; uint8_t brCmd[2] = { CMD_SET_BRIGHTNESS, 128 }; - if (!sendCmd(brCmd, 2)) { + if (!sendWithRetry(brCmd, 2)) { ESP_LOGE(TAG, "LCD setBrightness failed at 0x%02X", addr_); dev_ = nullptr; return false; } uint8_t rotCmd[2] = { CMD_SET_ROTATION, 0x00 }; - if (!sendCmd(rotCmd, 2)) { + if (!sendWithRetry(rotCmd, 2)) { ESP_LOGE(TAG, "LCD setRotation failed at 0x%02X", addr_); dev_ = nullptr; return false; diff --git a/Libraries/M5UnitModules/Source/UnitPaHub.cpp b/Libraries/M5UnitModules/Source/UnitPaHub.cpp index 1bac3ff..6b6f89f 100644 --- a/Libraries/M5UnitModules/Source/UnitPaHub.cpp +++ b/Libraries/M5UnitModules/Source/UnitPaHub.cpp @@ -4,9 +4,12 @@ static constexpr auto* TAG = "UnitPaHub"; bool UnitPaHub::begin(Device* dev, uint8_t addr) { + dev_ = nullptr; + addr_ = 0; + channel_ = NO_CHANNEL; if (!dev || !device_is_ready(dev)) return false; if (!unitProbe(dev, addr)) { - ESP_LOGW(TAG, "PaHub not found at 0x%02X", addr); + ESP_LOGD(TAG, "PaHub not found at 0x%02X", addr); return false; } dev_ = dev; @@ -22,21 +25,80 @@ bool UnitPaHub::begin(Device* dev, uint8_t addr) { return true; } +bool UnitPaHub::readChannel(uint8_t& mask) { + mask = 0; + if (!dev_) return false; + return i2c_controller_read(dev_, addr_, &mask, 1, + pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE; +} + bool UnitPaHub::select(uint8_t channel) { if (!dev_ || channel >= NUM_CHANNELS) return false; - // TCA9548A: write one byte - bit i set = channel i enabled - uint8_t mask = (uint8_t)(1u << channel); - bool ok = i2c_controller_write(dev_, addr_, &mask, 1, - pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE; - if (ok) channel_ = channel; - return ok; + // Writing the same mask again briefly disconnects and reconnects the + // downstream bus, which can make the next transaction fail. Skip it. + if (channel_ == channel) return true; + // PCA9548AP: write one byte - bit i set = channel i enabled. + const uint8_t mask = (uint8_t)(1u << channel); + bool wrote = false; + for (int attempt = 0; attempt < 3; attempt++) { + wrote = i2c_controller_write(dev_, addr_, &mask, 1, + pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE; + if (wrote) break; + if (attempt + 1 < 3) vTaskDelay(pdMS_TO_TICKS(1)); + } + if (!wrote) { + // The hardware state is unknown, so force the next select() to write + // the mask again instead of trusting the cache. + channel_ = NO_CHANNEL; + ESP_LOGW(TAG, "PaHub select CH%u write failed at 0x%02X", + (unsigned)channel, addr_); + return false; + } + channel_ = channel; + // Give the newly connected downstream bus a moment to settle. One + // millisecond was not enough for some STM32 units: their first version + // read after a channel change could NACK while the listener re-armed. + vTaskDelay(pdMS_TO_TICKS(3)); + + // Read the control register back for diagnostics only. It cannot be a hard + // gate: once a channel with an STM32 unit is selected, that unit can hold + // SCL low (clock stretching) and block the upstream readback even though + // the mux latched correctly. The PaHub test logs this value; identity + // checks in Unit8Encoder/UnitLcd prevent a wrong channel from being used. + uint8_t readback = 0; + if (readChannel(readback) && readback != mask) { + ESP_LOGD(TAG, "PaHub select CH%u readback 0x%02X (expected 0x%02X)", + (unsigned)channel, readback, mask); + } + return true; } bool UnitPaHub::deselect() { if (!dev_) return false; uint8_t mask = 0x00; - bool ok = i2c_controller_write(dev_, addr_, &mask, 1, - pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE; - if (ok) channel_ = NO_CHANNEL; - return ok; + uint8_t readback = 0; + bool readback_ok = false; + for (int attempt = 0; attempt < 3; attempt++) { + bool wrote = i2c_controller_write(dev_, addr_, &mask, 1, + pdMS_TO_TICKS(UNIT_I2C_TIMEOUT_MS)) == ERROR_NONE; + if (wrote) { + vTaskDelay(pdMS_TO_TICKS(1)); + readback_ok = readChannel(readback); + if (readback_ok && readback == 0) { + channel_ = NO_CHANNEL; + return true; + } + } + if (attempt + 1 < 3) vTaskDelay(pdMS_TO_TICKS(1)); + } + // Invalidate even on failure: a partial write may have changed the + // hardware, and select() must not skip the next write because of a stale + // cached channel. + channel_ = NO_CHANNEL; + if (readback_ok) { + ESP_LOGW(TAG, "PaHub deselect failed at 0x%02X (readback 0x%02X)", addr_, readback); + } else { + ESP_LOGW(TAG, "PaHub deselect failed at 0x%02X (readback failed)", addr_); + } + return false; } diff --git a/Libraries/M5UnitModules/Source/UnitRfid2.cpp b/Libraries/M5UnitModules/Source/UnitRfid2.cpp index ffc8c56..c21c56e 100644 --- a/Libraries/M5UnitModules/Source/UnitRfid2.cpp +++ b/Libraries/M5UnitModules/Source/UnitRfid2.cpp @@ -59,10 +59,23 @@ void UnitRfid2::writeFifo(const uint8_t* buf, uint8_t len) { // --------------------------------------------------------------------------- bool UnitRfid2::softReset() { - writeReg(REG_COMMAND, CMD_SOFT_RESET); + // Match the reference MFRC522 driver: clear pending ComIrq flags before + // issuing SoftReset. Some WS1850S boards otherwise leave the command + // register in a stale state after power-up. + uint8_t irq = 0x7F; + if (!unitWriteReg(dev_, addr_, REG_COM_IRQ, &irq, 1)) return false; + uint8_t reset = CMD_SOFT_RESET; + if (!unitWriteReg(dev_, addr_, REG_COMMAND, &reset, 1)) return false; vTaskDelay(pdMS_TO_TICKS(50)); for (int i = 0; i < 10; i++) { - if (!(readReg(REG_COMMAND) & 0x10)) return true; + uint8_t command = 0; + // Treat a failed read as "still busy" instead of success: readReg() + // returns 0 on a bus error, and CommandReg bit 4 == 0 would otherwise + // make a dead bus look like a completed reset. + if (unitReadReg(dev_, addr_, REG_COMMAND, &command, 1) && + !(command & 0x10)) { + return true; + } vTaskDelay(pdMS_TO_TICKS(10)); } return false; @@ -70,11 +83,32 @@ bool UnitRfid2::softReset() { void UnitRfid2::antennaOn() { uint8_t val = readReg(REG_TX_CONTROL); - if (!(val & 0x03)) - setBitMask(REG_TX_CONTROL, 0x03); + if ((val & 0x03) != 0x03) + writeReg(REG_TX_CONTROL, val | 0x03); +} + +bool UnitRfid2::readVersion(uint8_t& version) { + version = 0; + if (!dev_) return false; + // The WS1850S can miss the first version read on a freshly powered or + // freshly reset bus; the reference library retries this register too. + for (int attempt = 0; attempt < 3; attempt++) { + version = readReg(REG_VERSION); + // The RFID2 Unit uses a WS1850S, which reports 0x15. The remaining values + // cover MFRC522/PN512-family readers. + if (version == 0x15 || version == 0x88 || version == 0x90 || + version == 0x91 || version == 0x92 || version == 0x12) { + return true; + } + vTaskDelay(pdMS_TO_TICKS(1)); + } + return false; } bool UnitRfid2::begin(Device* dev, uint8_t addr) { + dev_ = nullptr; + addr_ = 0; + version_ = 0; if (!dev || !device_is_ready(dev)) return false; if (!unitProbe(dev, addr)) { ESP_LOGW(TAG, "RFID2 not found at 0x%02X", addr); @@ -83,12 +117,29 @@ bool UnitRfid2::begin(Device* dev, uint8_t addr) { dev_ = dev; addr_ = addr; + uint8_t version = 0; + if (!readVersion(version)) { + ESP_LOGW(TAG, "RFID2 identity check failed at 0x%02X (VersionReg 0x%02X)", + addr_, (unsigned)version); + dev_ = nullptr; + return false; + } + if (!softReset()) { ESP_LOGE(TAG, "RFID2 soft reset failed at 0x%02X", addr_); dev_ = nullptr; return false; } + // Verify the chip is still answering after the reset; readVersion() would + // otherwise treat a failed read as zero and let a dead bus pass. + if (!readVersion(version)) { + ESP_LOGE(TAG, "RFID2 lost identity after soft reset at 0x%02X", addr_); + dev_ = nullptr; + return false; + } + version_ = version; + // Timer: auto mode, prescaler for ~25ms timeout writeReg(REG_TMODE, 0x80); writeReg(REG_TPRESCALER, 0xA9); @@ -100,7 +151,8 @@ bool UnitRfid2::begin(Device* dev, uint8_t addr) { writeReg(REG_MODE, 0x3D); antennaOn(); - ESP_LOGI(TAG, "RFID2 ready at 0x%02X", addr_); + ESP_LOGI(TAG, "RFID2 ready at 0x%02X (fw 0x%02X, tx 0x%02X)", + addr_, (unsigned)version_, (unsigned)readReg(REG_TX_CONTROL)); return true; } @@ -151,8 +203,11 @@ uint8_t UnitRfid2::transceive(const uint8_t* txBuf, uint8_t txLen, } clearBitMask(REG_BIT_FRAMING, 0x80); - if (!(irq & 0x01) && (irq & 0x30) == 0) return 0; - if (readReg(REG_ERROR) & 0x1B) return 0; + if ((irq & 0x30) == 0) return 0; // no RxIRq and no IdleIRq + // Ignore CollErr (0x08): it is expected/possible during anticollision and + // the reference MFRC522 driver only treats BufferOvfl/Parity/Protocol as + // fatal. Rejecting CollErr here made a single card look like no card at all. + if (readReg(REG_ERROR) & 0x13) return 0; uint8_t n = readReg(REG_FIFO_LEVEL) & 0x7F; if (n > rxMaxLen) n = rxMaxLen; @@ -198,6 +253,7 @@ bool UnitRfid2::transceiveCRC(const uint8_t* txBuf, uint8_t txLen, // Send a 7-bit short frame command (REQA=0x26 or WUPA=0x52). // REQA only wakes IDLE cards; WUPA wakes both IDLE and HALT cards. bool UnitRfid2::requestA(uint8_t atqa[2], uint8_t cmd) { + writeReg(REG_COLL, 0x00); // ValuesAfterColl=0 while waiting for ATQA writeReg(REG_BIT_FRAMING, 0x07); uint8_t rx[2] = {}; uint8_t rxLen = transceive(&cmd, 1, rx, 2); @@ -217,7 +273,7 @@ bool UnitRfid2::select(Uid* uid) { uint8_t uidSize = 0; for (int cascade = 0; cascade < 3; cascade++) { - writeReg(REG_COLL, 0x80); // ValuesAfterColl: don't clear bits on collision + writeReg(REG_COLL, 0x00); // ValuesAfterColl=0: keep collision bits writeReg(REG_BIT_FRAMING, 0x00); uint8_t anticollCmd[2] = { selCmd[cascade], PICC_ANTICOLL }; diff --git a/Libraries/M5UnitModules/UnitReferences.md b/Libraries/M5UnitModules/UnitReferences.md index 46777e2..aacbf80 100644 --- a/Libraries/M5UnitModules/UnitReferences.md +++ b/Libraries/M5UnitModules/UnitReferences.md @@ -14,9 +14,31 @@ Each entry links to the official M5Stack docs and the upstream Arduino library. | Channels | 6 (CH0-CH5) | | Driver class | `UnitPaHub` | -PCA9548A-based I2C multiplexer. Allows up to 6 I2C devices with the same -address to share a single bus. Select a channel before talking to a device -behind it; deselect (mask = 0) when done. +PCA9548AP-based I2C multiplexer. The chip supports eight channels, but this unit +wires only six (CH0-CH5), so up to 6 I2C devices with the same address can share +a single bus. Select a channel before talking to a device behind it; deselect +(mask = 0) when done. + +`select()` skips the write when the requested channel is already cached as +active (writing the same mask again briefly reconnects the downstream bus), +and waits 3 ms after a real channel change for the bus to settle. It reads the +control register back for diagnostics, but does not fail on a mismatch: a +downstream STM32 unit can hold SCL low and block the upstream readback even +though the mux latched correctly. `deselect()` writes mask 0 and requires a +0x00 readback, because with no channel selected the upstream bus should be +free; this also prevents a non-mux device at 0x70 from passing `begin()`. A +failed write or deselect invalidates the cached channel, so the next +`select()` always writes the mask again. + +The M5UnitTest PaHub scanner probes one channel per tap. It waits 20 ms after +selecting a channel and goes straight to the identity reads: a mux readback at +that point can be blocked by downstream clock stretching. If a full channel +scan misses, it disconnects the channel, waits, re-selects it, and scans once +more before reporting "No devices found". Every address in `KNOWN_UNIT_ADDRS` +uses an identity read: firmware version for +8Encoder/Scroll/ByteButton/Joystick2/CardKB2, four-byte ID for the LCD, and the +MFRC522/WS1850S `VersionReg` for RFID2. Only an identity match is reported as +found. - Docs: https://docs.m5stack.com/en/unit/Unit-PaHub%20v2.1 - Repo: https://github.com/m5stack/M5Unit-HUB @@ -25,7 +47,7 @@ behind it; deselect (mask = 0) when done. | Addr | Operation | Description | |------|-----------|-------------| -| 0x70 | W: bitmask | Enable channels (bit i = channel i). Write 0x00 to deselect all. | +| 0x70 | R/W: bitmask | Enable channels (bit i = channel i). Write 0x00 to deselect all. Read back to verify the latch. | --- @@ -50,6 +72,11 @@ reboots. **I2C protocol:** single I2C read (no register write) returns current key ASCII value. 0x00 = no key or I2C error (indistinguishable by design). +**Identity register:** write `0xF1`, then read one byte to get the firmware +version (current firmware returns `0x02`). `UnitCardKB2::begin()` requires a +version in the range 1-15, and `firmwareVersion()` exposes it. Reading `0xF1` +does not consume a queued key. + **UART protocol:** 5-byte frames: `AA 03 [KEY_ID] [KEY_STATE] [checksum]` - `KEY_ID` = row × 11 + col (0–43, rows 0–3, cols 0–10) - `KEY_STATE` = 0x01 pressed, 0x02 released @@ -81,6 +108,11 @@ value. 0x00 = no key or I2C error (indistinguishable by design). Internal ESP32 bridges I2C commands to the ST7789V2. Commands are fire-and-forget (the ESP32 processes them asynchronously). For bulk pixel writes, poll `bufferRemaining()` (register 0x09) to avoid overflow. +`begin()` first reads the four-byte ID (`0x04`; must start `0x77 0x89`) and +retries up to three times. This identity check prevents a false ACK on a +floating PaHub channel from being treated as an LCD. It then retries the +brightness and rotation commands up to three times, because the controller can +still be busy when a PaHub channel has just been switched. - Docs: https://docs.m5stack.com/en/unit/lcd - M5GFX repo: https://github.com/m5stack/M5GFX @@ -89,6 +121,7 @@ writes, poll `bufferRemaining()` (register 0x09) to avoid overflow. | Cmd | Bytes | Description | |-----|-------|-------------| +| 0x04 | (read 4 bytes) | READ_ID: `0x77 0x89 [version hi] [version lo]` | | 0x22 | [brightness] | Set backlight (0=off, 255=max) | | 0x36 | [rotation] | Set rotation (0=portrait, 1=landscape, 2=portrait flip, 3=landscape flip) | | 0x6A | [x0][y0][x1][y1][hi][lo] | Fill rect with RGB565 inline | @@ -206,7 +239,7 @@ and one RGB LED for the switch. Total 9 addressable LEDs. | 0x60 | R | uint8_t | Toggle switch state (0=off, 1=on) | | 0x70 + i*3 | R/W | 3 bytes | LED [R, G, B] for encoder i (i = 0-7) | | 0x88 | R/W | 3 bytes | LED [R, G, B] for toggle switch (LED8) | -| 0xF0 | R | uint8_t | Firmware version | +| 0xFE | R | uint8_t | Firmware version | | 0xFF | R/W | uint8_t | I2C address | Hardware counts 4 pulses per detent; the driver divides by 4 to give @@ -214,6 +247,13 @@ detent-level deltas. Counter range: -2,147,483,648 to +2,147,483,647. **LED indices in `Unit8Encoder`:** 0-7 = encoder LEDs, 8 = switch LED. +`begin()` validates the device by reading the firmware-version register (`0xFE`, +accepted range 1-15) and the address register (`0xFF`, must match the probed +address). It then retries the encoder-LED and switch-LED init writes up to three +times. This identity check is what prevents a phantom ACK on a floating PaHub +channel from latching the scan onto the wrong port; it rejects the device only +if the LED writes still fail after the identity check passes. + --- ## ByteButton Unit @@ -320,6 +360,10 @@ Classic, MIFARE Ultralight, and ISO 14443-A cards. - The MFRC522 supports SPI, I2C, and UART; this unit hardwires I2C mode. - UID length: 4 bytes (single-size) or 7 bytes (double-size) depending on card. - Always check `readUID()` return length; 0 = no card present or read error. +- Identity register: `0x37` (`VersionReg`). The RFID2 Unit's WS1850S reports + `0x15`; MFRC522/PN512-family readers report `0x88`, `0x90`, `0x91`, `0x92`, + or `0x12`. `0x00`/`0xFF` mean no valid reader. + `UnitRfid2::firmwareVersion()` exposes the value read during `begin()`. --- @@ -327,14 +371,18 @@ Classic, MIFARE Ultralight, and ISO 14443-A cards. All drivers use `UnitCommon.h` helpers: -- `unitProbe(dev, addr)` - checks for device ACK at address -- `unitReadReg(dev, addr, reg, buf, len)` - STM32-style: write reg byte, then read +- `unitProbe(dev, addr)` - performs a real identity read and checks for a valid response, retrying up to five times (first success wins). It reads the PaHub control register, a firmware-version register (`0xFE`, version 1-15) for the 8Encoder/Scroll/ByteButton/Joystick2, a four-byte ID (`0x04` -> `0x77 0x89 ...`) for the LCD, CardKB2 version `0xF1` (1-15), and RFID2 `VersionReg` `0x37` (WS1850S `0x15` or an MFRC522-family value). It does not use the unreliable ESP-IDF `i2c_master_probe()` path. +- `unitReadReg(dev, addr, reg, buf, len)` - STM32-style: write reg byte, STOP, 3 ms delay, then read - `unitWriteReg(dev, addr, reg, buf, len)` - write reg byte followed by data - `UNIT_I2C_TIMEOUT_MS` = 20 ms default timeout +- `UNIT_I2C_PROBE_TIMEOUT_MS` = 10 ms per probe attempt +- `UNIT_I2C_PROBE_ATTEMPTS` = 5 +- `UNIT_I2C_PROBE_RETRY_DELAY_MS` = 2 ms between failed attempts The STM32-based units (8Encoder, ByteButton, Scroll, Joystick2, CardKB2) -require a 2 ms delay between write (reg select) and read in some cases. This -is handled internally by the I2C controller driver. +require a 3 ms delay between the register-pointer write and the read. This +is handled by `unitReadReg()`. RFID2 uses the same pointer-write/read sequence +for its MFRC522 registers. -When using the PaHub, always call `selectIfNeeded()` **before** `isPresent()` -to ensure the correct channel is active for the ACK check. +When using the PaHub, always call `select()` **before** probing or accessing +a unit, so the correct channel is active for the ACK check. diff --git a/Libraries/SfxEngine/Include/SfxDefinitions.h b/Libraries/SfxEngine/Include/SfxDefinitions.h index c3c55f0..68a272a 100644 --- a/Libraries/SfxEngine/Include/SfxDefinitions.h +++ b/Libraries/SfxEngine/Include/SfxDefinitions.h @@ -2,7 +2,7 @@ * @file SfxDefinitions.h * @brief 52 curated SFX definitions for SfxEngine. * - * Implements SfxEngine::loadSound() — mapping SfxId values to note sequences. + * Implements SfxEngine::loadSound() - mapping SfxId values to note sequences. */ #pragma once @@ -20,16 +20,20 @@ } \ } while(0) -inline void SfxEngine::loadSound(SfxId sound) { +inline void SfxEngine::loadSound(SfxId sound, int slot) { + SequencePlayer& player = sequencePlayers_[slot]; + // Local reference aliases matching the old member names, so every ADD_SFX_NOTE/ + // sequenceLength_ line below (one per sound, 52 cases) needs no per-case changes: + // they now write into this call's own slot instead of engine-wide shared state. + auto& sequence_ = player.notes; + auto& sequenceLength_ = player.length; + auto& sequenceIndex_ = player.index; + auto& sequenceDelaySamples_ = player.delaySamples; + sequenceIndex_ = 0; sequenceDelaySamples_ = 0; sequenceLength_ = 0; - - // Stop all voices - for (int i = 0; i < NUM_VOICES; i++) { - voices_[i].active = false; - voices_[i].envStage = EnvelopeStage::Off; - } + player.startOrder = nextSequenceOrder_++; switch (sound) { diff --git a/Libraries/SfxEngine/Include/SfxEngine.h b/Libraries/SfxEngine/Include/SfxEngine.h index 8002f3a..42f7de8 100644 --- a/Libraries/SfxEngine/Include/SfxEngine.h +++ b/Libraries/SfxEngine/Include/SfxEngine.h @@ -217,6 +217,9 @@ class SfxEngine { int decaySamples = 0; float sustainLevel = 0.7f; int releaseSamples = 0; + // Twang envelope: per-sample decay multiplier, derived from a decay time at trigger + // (a fixed per-sample constant would depend on the sample rate). + float envDecayMul = 0.998f; // Volume float volume = 0.5f; @@ -236,10 +239,25 @@ class SfxEngine { // SFX Sequence //-------------------------------------------------------------------------- - SfxSequenceNote sequence_[MAX_SFX_NOTES] = {}; - int sequenceLength_ = 0; - int sequenceIndex_ = 0; - int sequenceDelaySamples_ = 0; + // Independent concurrent sequence players: play() claims a free (or, if none free, the + // oldest-started) slot rather than sharing one global timeline. Without this, a play() + // call while a previous sound was still mid-sequence would silently cancel it (loadSound() + // used to reset every voice and replace the one shared sequence_[]/cursor). Each slot + // owns MAX_VOICES_PER_SEQUENCE physical voices so layered notes (note.voice > 0, e.g. + // the Error sound's second layer) sound together instead of stealing each other. + static constexpr int MAX_VOICES_PER_SEQUENCE = 2; + static constexpr int MAX_CONCURRENT_SEQUENCES = NUM_VOICES / MAX_VOICES_PER_SEQUENCE; + + struct SequencePlayer { + SfxSequenceNote notes[MAX_SFX_NOTES] = {}; + int length = 0; + int index = 0; + int delaySamples = 0; + uint32_t startOrder = 0; // for oldest-slot eviction when all slots are active + }; + + SequencePlayer sequencePlayers_[MAX_CONCURRENT_SEQUENCES] = {}; + uint32_t nextSequenceOrder_ = 0; //-------------------------------------------------------------------------- // Queue Messages @@ -276,6 +294,11 @@ class SfxEngine { AudioStreamHandle audioStreamHandle_ = nullptr; TaskHandle_t task_ = nullptr; SemaphoreHandle_t stopSemaphore_ = nullptr; // Signaled when audio task exits + // Signaled once by the audio task after its own initial open attempt (matching DoomAudio's + // pattern of opening the stream from inside its own pinned, high-priority mixer task + // instead of the caller of start()), so start() can still report success/failure synchronously. + SemaphoreHandle_t startSemaphore_ = nullptr; + volatile bool startOpenSucceeded_ = false; QueueHandle_t msgQueue_ = nullptr; volatile bool running_ = false; @@ -328,16 +351,24 @@ class SfxEngine { // Sound Loading //-------------------------------------------------------------------------- - void loadSound(SfxId sound); + // Loads `sound`'s notes into sequencePlayers_[slot], replacing whatever that slot was + // previously playing. Other slots (and the voices they're using) are untouched. + void loadSound(SfxId sound, int slot); void triggerNote(Voice& v, uint8_t midiNote, uint16_t durationMs, SfxWaveType wave, float volume, SfxEnvelopeType envType, uint16_t attackMs = 0, uint16_t decayMs = 0, float sustain = 0.0f, uint16_t releaseMs = 0); - void processSequence(); + void processSequence(SequencePlayer& player, int slot); + // Returns a free sequence-player slot, or the oldest-started one if all are active. + int allocateSequenceSlot(); //-------------------------------------------------------------------------- // Task //-------------------------------------------------------------------------- + // Finds the current audio-stream device and opens an output stream on it. + // Shared by start() and the audio task's re-open-after-codec-rebind path. + bool openStream(); + static void audioTaskFunc(void* param); }; diff --git a/Libraries/SfxEngine/README.md b/Libraries/SfxEngine/README.md index ab716ca..e42f3f0 100644 --- a/Libraries/SfxEngine/README.md +++ b/Libraries/SfxEngine/README.md @@ -62,7 +62,6 @@ file(GLOB_RECURSE SFX_ENGINE_FILES ../../../Libraries/SfxEngine/Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} ${SFX_ENGINE_FILES} INCLUDE_DIRS - ../../../Libraries/TactilityCpp/Include ../../../Libraries/SfxEngine/Include REQUIRES TactilitySDK esp_driver_i2s esp_driver_gpio ) diff --git a/Libraries/SfxEngine/Source/SfxEngine.cpp b/Libraries/SfxEngine/Source/SfxEngine.cpp index 1ae57fa..940421b 100644 --- a/Libraries/SfxEngine/Source/SfxEngine.cpp +++ b/Libraries/SfxEngine/Source/SfxEngine.cpp @@ -2,7 +2,7 @@ * @file SfxEngine.cpp * @brief Lightweight SFX-only audio engine implementation. * - * Stripped-down version of SoundEngine — no BGM, no delay, no advanced synthesis. + * Stripped-down version of SoundEngine - no BGM, no delay, no advanced synthesis. * Sound definitions are in SfxDefinitions.h (inline loadSound). */ @@ -121,7 +121,7 @@ void SfxEngine::updateEnvelope(Voice& v) { return; } else if (v.envType == SfxEnvelopeType::Twang) { if (v.envSamplePos == 1) v.envLevel = 1.0f; - v.envLevel *= 0.992f; + v.envLevel *= v.envDecayMul; if (v.envLevel < 0.001f) { v.envLevel = 0.0f; v.envStage = EnvelopeStage::Off; @@ -304,7 +304,9 @@ float SfxEngine::generateVoiceSample(Voice& v) { void SfxEngine::fillStereoBuffer(int16_t* buf, int samples) { for (int i = 0; i < samples; i++) { - processSequence(); + for (int slot = 0; slot < MAX_CONCURRENT_SEQUENCES; slot++) { + processSequence(sequencePlayers_[slot], slot); + } float mix = 0.0f; @@ -340,6 +342,7 @@ void SfxEngine::fillStereoBuffer(int16_t* buf, int samples) { // Convert to 16-bit stereo int16_t s = static_cast(mix * 28000); + buf[i * 2] = s; buf[i * 2 + 1] = s; } @@ -380,6 +383,11 @@ void SfxEngine::triggerNote(Voice& v, uint8_t midiNote, uint16_t durationMs, v.releaseSamples = (50 * SAMPLE_RATE) / 1000; } + // Twang decays exponentially. Derive the per-sample multiplier from a time constant so + // the decay is independent of the sample rate (0.992/sample was -60 dB in ~54 ms at 16 kHz). + float twangDecaySeconds = (decayMs > 0) ? (decayMs / 1000.0f) : 0.35f; + v.envDecayMul = powf(0.001f, 1.0f / (twangDecaySeconds * SAMPLE_RATE)); + // Reset pitch effects v.vibratoDepth = 0.0f; v.vibratoRate = 0.0f; @@ -393,27 +401,52 @@ void SfxEngine::triggerNote(Voice& v, uint8_t midiNote, uint16_t durationMs, // Sequence Processing //============================================================================== -void SfxEngine::processSequence() { - if (sequenceIndex_ >= sequenceLength_) return; +void SfxEngine::processSequence(SequencePlayer& player, int slot) { + if (player.index >= player.length) return; - if (sequenceDelaySamples_ > 0) { - sequenceDelaySamples_--; + if (player.delaySamples > 0) { + player.delaySamples--; return; } - const SfxSequenceNote& note = sequence_[sequenceIndex_]; + const SfxSequenceNote& note = player.notes[player.index]; - if (note.voice < NUM_VOICES && note.pitch > 0) { - triggerNote(voices_[note.voice], note.pitch, note.durationMs, + // Each slot owns a stride of MAX_VOICES_PER_SEQUENCE voices, so layered + // definitions (notes with voice 1, e.g. Error's second layer) sound on + // their own voice instead of retriggering - and killing - voice 0's + // still-sounding envelope. Out-of-range voice indices are dropped: all + // shipped definitions stay within the stride. + if (note.pitch > 0 && note.voice < MAX_VOICES_PER_SEQUENCE) { + triggerNote(voices_[slot * MAX_VOICES_PER_SEQUENCE + note.voice], note.pitch, note.durationMs, note.wave, note.volume, note.envType, note.attackMs, note.decayMs, note.sustain, note.releaseMs); } - sequenceIndex_++; + player.index++; + + if (player.index < player.length) { + player.delaySamples = (player.notes[player.index].delayMs * SAMPLE_RATE) / 1000; + } +} + +// Returns a free sequence-player slot (length == 0, i.e. finished or never started), or the +// oldest-started slot if all MAX_CONCURRENT_SEQUENCES are still active - matching the engine's +// existing "steal the longest-running thing" behavior elsewhere (e.g. voice allocation is +// implicit per-note via the sequence's own voice index). +int SfxEngine::allocateSequenceSlot() { + for (int i = 0; i < MAX_CONCURRENT_SEQUENCES; i++) { + if (sequencePlayers_[i].index >= sequencePlayers_[i].length) { + return i; + } + } - if (sequenceIndex_ < sequenceLength_) { - sequenceDelaySamples_ = (sequence_[sequenceIndex_].delayMs * SAMPLE_RATE) / 1000; + int oldest = 0; + for (int i = 1; i < MAX_CONCURRENT_SEQUENCES; i++) { + if (sequencePlayers_[i].startOrder < sequencePlayers_[oldest].startOrder) { + oldest = i; + } } + return oldest; } //============================================================================== @@ -428,12 +461,26 @@ void SfxEngine::audioTaskFunc(void* param) { ESP_LOGI(TAG, "Audio task started"); + // Opens the stream from inside this task (matching DoomAudio's mixer task, which opens and + // writes from the same pinned, high-priority task context throughout) rather than the + // caller of start() opening it beforehand on a different task. + self->startOpenSucceeded_ = self->openStream(); + xSemaphoreGive(self->startSemaphore_); + if (!self->startOpenSucceeded_) { + self->running_ = false; + if (self->stopSemaphore_ != nullptr) { + xSemaphoreGive(self->stopSemaphore_); + } + vTaskDelete(nullptr); + return; + } + while (self->running_) { // Process queued messages (non-blocking) while (xQueueReceive(self->msgQueue_, &msg, 0) == pdTRUE) { switch (msg.type) { case MsgType::PlaySound: - self->loadSound(msg.sfxId); + self->loadSound(msg.sfxId, self->allocateSequenceSlot()); break; case MsgType::PlayNote: @@ -457,7 +504,10 @@ void SfxEngine::audioTaskFunc(void* param) { self->voices_[i].envStage = EnvelopeStage::Release; self->voices_[i].envSamplePos = 0; } - self->sequenceLength_ = 0; + for (auto& player : self->sequencePlayers_) { + player.length = 0; + player.index = 0; + } break; } } @@ -483,15 +533,37 @@ void SfxEngine::audioTaskFunc(void* param) { error_t error = audio_stream_write(self->audioStreamHandle_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(100)); if (error != ERROR_NONE) { - ESP_LOGE(TAG, "Audio stream write error"); - self->running_ = false; - break; + // ERROR_INVALID_STATE/ERROR_RESOURCE here typically mean a hotplug codec change + // (e.g. USB audio attach/detach) re-bound the output direction out from under this + // handle - the old handle is now permanently dead, but a new one against whatever + // codec is current should work. Close it and retry a bounded number of times before + // giving up, rather than killing the task on the first hot-swap. + ESP_LOGW(TAG, "Audio stream write error (%s) - reopening", error_to_string(error)); + audio_stream_close(self->audioStreamHandle_); + self->audioStreamHandle_ = nullptr; + self->audioStreamDevice_ = nullptr; + + bool reopened = false; + for (int attempt = 0; attempt < 10 && self->running_; attempt++) { + if (self->openStream()) { + reopened = true; + break; + } + vTaskDelay(pdMS_TO_TICKS(100)); + } + if (!reopened) { + ESP_LOGE(TAG, "Failed to reopen audio stream after codec change - stopping"); + self->running_ = false; + break; + } } } - // Flush silence - memset(self->audioBuffer_, 0, sizeof(self->audioBuffer_)); - audio_stream_write(self->audioStreamHandle_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50)); + // Flush silence (skip if the stream is already gone - e.g. reopen failed above) + if (self->audioStreamHandle_ != nullptr) { + memset(self->audioBuffer_, 0, sizeof(self->audioBuffer_)); + audio_stream_write(self->audioStreamHandle_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50)); + } ESP_LOGI(TAG, "Audio task exiting"); @@ -507,9 +579,7 @@ void SfxEngine::audioTaskFunc(void* param) { // Public API //============================================================================== -bool SfxEngine::start() { - if (running_) return true; - +bool SfxEngine::openStream() { // Find audio stream device audioStreamDevice_ = nullptr; device_for_each_of_type(&AUDIO_STREAM_TYPE, &audioStreamDevice_, [](Device* device, void* context) { @@ -538,28 +608,57 @@ bool SfxEngine::start() { return false; } + return true; +} + +bool SfxEngine::start() { + if (running_) return true; + // Create message queue msgQueue_ = xQueueCreate(8, sizeof(QueueMsg)); if (msgQueue_ == nullptr) { ESP_LOGE(TAG, "Failed to create message queue"); - audio_stream_close(audioStreamHandle_); - audioStreamHandle_ = nullptr; - audioStreamDevice_ = nullptr; return false; } - // Start audio task + startSemaphore_ = xSemaphoreCreateBinary(); + if (startSemaphore_ == nullptr) { + ESP_LOGE(TAG, "Failed to create start semaphore"); + vQueueDelete(msgQueue_); + msgQueue_ = nullptr; + return false; + } + + // Start audio task. It opens the stream itself as its first action (matching DoomAudio's + // mixer task, which opens and writes from the same pinned, high-priority task context + // throughout) and signals startSemaphore_ with the result. systemVolumePollCounter_ = 0; // poll the system volume immediately on the first iteration running_ = true; - BaseType_t result = xTaskCreate(audioTaskFunc, "sfxeng", 4096, this, 5, &task_); + // Pinned to core 0 at a high priority (matching DoomAudio's mixer task): USB Audio pulls + // from its ring buffer on a fixed real-time schedule regardless of what the app is doing, + // and silently drops audio if the ring runs dry. At the default priority/no pinning, the + // app's own game/render work could delay this task's next buffer-fill+write long enough to + // starve the ring - audible as playback "cutting in and out" when output is a USB device. + BaseType_t result = xTaskCreatePinnedToCore(audioTaskFunc, "sfxeng", 4096, this, 10, &task_, 0); if (result != pdPASS) { ESP_LOGE(TAG, "Failed to create audio task"); running_ = false; + vSemaphoreDelete(startSemaphore_); + startSemaphore_ = nullptr; vQueueDelete(msgQueue_); msgQueue_ = nullptr; - audio_stream_close(audioStreamHandle_); - audioStreamHandle_ = nullptr; - audioStreamDevice_ = nullptr; + return false; + } + + xSemaphoreTake(startSemaphore_, portMAX_DELAY); + vSemaphoreDelete(startSemaphore_); + startSemaphore_ = nullptr; + + if (!startOpenSucceeded_) { + // The task already cleaned up running_/self-deleted on open failure. + vQueueDelete(msgQueue_); + msgQueue_ = nullptr; + task_ = nullptr; return false; } diff --git a/Libraries/UnitLcdGfx/Include/UnitLcdSink.h b/Libraries/UnitLcdGfx/Include/UnitLcdSink.h new file mode 100644 index 0000000..9af0bd5 --- /dev/null +++ b/Libraries/UnitLcdGfx/Include/UnitLcdSink.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include + +/** + * Presents a gfx::SinkTarget frame to an M5Stack LCD Unit over I2C. + * + * UnitLcd stays unchanged; this adapter lives outside M5UnitModules. The LCD must already be + * begun and set to the rotation whose width()/height() match the SinkTarget dimensions. + */ +class UnitLcdSink : public gfx::Sink { +public: + /** + * @param lcd the unit, already begun and rotated + * @param originX left edge of the sub-frame within the LCD + * @param originY top edge of the sub-frame within the LCD + */ + explicit UnitLcdSink(UnitLcd& lcd, int originX = 0, int originY = 0) + : lcd_(lcd), originX_(originX), originY_(originY) {} + + bool begin(const gfx::SinkInfo& info) override { + if (!lcd_.isPresent()) { + ESP_LOGW("UnitLcdSink", "begin: LCD not present"); + return false; + } + if (info.format != DISPLAY_COLOR_FORMAT_RGB565) { + ESP_LOGW("UnitLcdSink", "begin: format %d is not RGB565", static_cast(info.format)); + return false; + } + if (originX_ < 0 || originY_ < 0 || info.width <= 0 || info.height <= 0) { + ESP_LOGW("UnitLcdSink", "begin: invalid sub-frame %dx%d at (%d, %d)", info.width, info.height, originX_, originY_); + return false; + } + if (originX_ + info.width > static_cast(lcd_.width()) || originY_ + info.height > static_cast(lcd_.height())) { + ESP_LOGW( + "UnitLcdSink", + "begin: sub-frame %dx%d at (%d, %d) exceeds LCD %ux%u", + info.width, + info.height, + originX_, + originY_, + static_cast(lcd_.width()), + static_cast(lcd_.height()) + ); + return false; + } + info_ = info; + begun_ = true; + return true; + } + + bool present(const struct PixelBuffer& buffer, const gfx::Rect& rect) override { + if (!begun_) { + return false; + } + + const gfx::Rect clipped = rect.intersect(gfx::Rect { 0, 0, info_.width, info_.height }); + if (clipped.empty()) { + return true; + } + + if (!lcd_.setWindow( + static_cast(originX_ + clipped.x0), + static_cast(originY_ + clipped.y0), + static_cast(originX_ + clipped.x1 - 1), + static_cast(originY_ + clipped.y1 - 1))) { + return false; + } + + auto* mutable_buffer = const_cast(&buffer); + const int bufferWidth = pixel_buffer_get_width(&buffer); + const std::size_t stride = pixel_buffer_get_stride_bytes(&buffer); + const auto* data = static_cast(pixel_buffer_get_data(mutable_buffer)); + + // Full-width rows are contiguous in a PixelBuffer. Send them as one stream instead of one + // I2C transaction per row; the first full-frame present is much faster this way. + if (clipped.x0 == 0 && clipped.x1 == bufferWidth && stride == static_cast(bufferWidth) * sizeof(uint16_t)) { + lcd_.writePixels( + data + static_cast(clipped.y0) * bufferWidth, + static_cast(bufferWidth) * static_cast(clipped.height()) + ); + return true; + } + + for (int y = clipped.y0; y < clipped.y1; y++) { + const auto* row = static_cast(pixel_buffer_get_row(mutable_buffer, y)); + lcd_.writePixels(row + clipped.x0, static_cast(clipped.width())); + } + return true; + } + + void end() override { + begun_ = false; + } + +private: + UnitLcd& lcd_; + int originX_ = 0; + int originY_ = 0; + gfx::SinkInfo info_ {}; + bool begun_ = false; +}; diff --git a/Libraries/UnitLcdGfx/README.md b/Libraries/UnitLcdGfx/README.md new file mode 100644 index 0000000..4bde297 --- /dev/null +++ b/Libraries/UnitLcdGfx/README.md @@ -0,0 +1,53 @@ +# UnitLcdGfx + +A `gfx::Sink` adapter that presents a `gfx::SinkTarget` frame to the M5Stack LCD Unit over I2C. +It deliberately lives outside `Libraries/M5UnitModules`; `UnitLcd` itself is unchanged. + +## Usage + +```cpp +#include +#include +#include + +UnitLcd lcd; +if (!lcd.begin(i2c_device, UnitLcd::DEFAULT_ADDR)) { /* not found */ } +lcd.setRotation(1); // landscape; width()/height() define the frame size + +UnitLcdSink sink(lcd); +gfx::SinkTarget target(sink, lcd.width(), lcd.height(), DISPLAY_COLOR_FORMAT_RGB565); +if (!target.valid()) { /* sink rejected the frame format/size */ } + +while (running) { + target.render([](gfx::Surface& surface) { + surface.clear(0x0000); + surface.drawText(4, 4, "hello", gfx::FONT_8X16, 0xFFFF); + }); +} +``` + +The `SinkTarget` owns a full RGB565 frame buffer and presents only dirty regions; the sink sets +the LCD window and streams each changed row with `UnitLcd::writePixels()`. + +## Build integration + +An app using this library must compile `UnitLcdGfx/Source` (currently header-only, so nothing +extra), `M5UnitModules/Source/UnitLcd.cpp`, `Libraries/Gfx/Source/*.c*`, and include: + +- `Libraries/Gfx/Include` +- `Libraries/M5UnitModules/Include` +- `Libraries/UnitLcdGfx/Include` + +The usual `REQUIRES graphics-module` and the platform I2C driver requirements from +`M5UnitModules` also apply. `UnitLcd` uses a 400 kHz I2C bus and a 135x240 (or rotated) panel, so +prefer dirty regions and avoid full-screen animation. + +## Notes + +- Only `DISPLAY_COLOR_FORMAT_RGB565` is supported, matching the unit's protocol. +- The unit's rotation must be set before creating the `SinkTarget`; `begin()` checks that the + requested frame size matches `lcd.width()`/`lcd.height()`. +- `UnitLcdSink` holds a reference to the `UnitLcd`; the LCD must outlive the sink and target. +- The unit's framebuffer persists across app restarts. If an app doesn't repaint every pixel, + call `lcd.fillScreen(0x0000)` at startup and shutdown (the demo does this) so the previous + image doesn't linger. diff --git a/Scripts/check-app-symbols.py b/Scripts/check-app-symbols.py new file mode 100644 index 0000000..c3a471a --- /dev/null +++ b/Scripts/check-app-symbols.py @@ -0,0 +1,723 @@ +#!/usr/bin/env python3 +""" +Check which symbols an ELF app needs that the firmware does not export. + +Apps are relocated at load time against the firmware's symbol tables. A symbol the app references +but the firmware does not export fails at *load* time with a bare + + E (9713) ELF: Can't find common xPortEnterCriticalTimeout + E (9716) ElfApp: Application failed to load: missing symbol + +which names one symbol per attempt, so finding them all by flashing is a slow loop. This reads the +app's ELF directly and reports every missing symbol at once, tells you which file to add it to, and +warns about the "funky" cases (mangled C++ names, symbols that exist in the source tree but whose +owning module is never started on the currently-built board). + +Usage: + python Scripts/check-app-symbols.py [--sdk ] + python Scripts/check-app-symbols.py --out report.txt + python Scripts/check-app-symbols.py --list-exports [--sdk ] + +Exit status is 1 when symbols are missing (or resolve only through an inactive module), so this can +gate a build. +""" + +import argparse +import re +import struct +import subprocess +import sys +from pathlib import Path + + +# --------------------------------------------------------------------------------------------- +# Discovering the firmware checkout +# --------------------------------------------------------------------------------------------- + +def find_firmware_root(explicit): + """Locates the Tactility checkout holding the export tables.""" + if explicit: + root = Path(explicit) + if not (root / "TactilityKernel").is_dir(): + raise SystemExit(f"{root} does not look like a Tactility checkout (no TactilityKernel/)") + return root + + here = Path(__file__).resolve() + candidates = [ + here.parent.parent.parent / "Tactility", + here.parent.parent, + ] + for candidate in candidates: + if (candidate / "TactilityKernel" / "source" / "module.cpp").is_file(): + return candidate + raise SystemExit("Could not locate Tactility next to this repo. Pass --sdk explicitly.") + + +# Directories under the SDK checkout that are never worth scanning for exports: build output +# (regenerated, huge, and duplicates the real sources) and third-party vendor trees pulled in by +# the component manager (their own internals aren't things an app should ever reference/export). +SKIP_DIR_NAMES = {"build", "managed_components", ".git", "cmake-build-debug"} + + +def _iter_source_files(root): + for path in root.rglob("*"): + if path.suffix not in (".c", ".cpp"): + continue + if any(part in SKIP_DIR_NAMES for part in path.parts): + continue + yield path + + +# --------------------------------------------------------------------------------------------- +# Parsing ModuleSymbol export tables out of arbitrary firmware source files +# --------------------------------------------------------------------------------------------- + +# DEFINE_MODULE_SYMBOL(name) -> { "name", (void*)&name } +MACRO_RE = re.compile(r"\bDEFINE_MODULE_SYMBOL\(\s*([A-Za-z_][A-Za-z_0-9]*)\s*\)") +# DEFINE_MODULE_SYMBOL_SIGNATURE(name, type) -> exported under bare `name`, address cast to `type` +# to disambiguate an overloaded symbol (e.g. picking one of several float/double overloads). +SIGNATURE_RE = re.compile(r"\bDEFINE_MODULE_SYMBOL_SIGNATURE\(\s*([A-Za-z_][A-Za-z_0-9]*)\s*,") +# DEFINE_MODULE_SYMBOL_ALIAS("name", expr) -> exported under the quoted string, not the C expr. +ALIAS_RE = re.compile(r'\bDEFINE_MODULE_SYMBOL_ALIAS\(\s*"([^"]+)"\s*,') +# Raw { "name", ... } literal table entries: used for forcing-wrapper functions (constructors / +# destructors with no out-of-line definition the compiler would otherwise emit an address for), +# for mangled C++ ABI names GCC can't spell as a C identifier, and for a handful of std:: globals. +LITERAL_RE = re.compile(r'\{\s*"([^"]+)"\s*,') +# Where a Module struct's `.symbols = X` (or positional 4th initializer) names its own symbol +# table, so a table can be tied back to the module (and therefore board) that owns it. +MODULE_STRUCT_RE = re.compile(r"\b(?:struct\s+)?Module\s+([A-Za-z_][A-Za-z_0-9]*)\s*=\s*\{") +SYMBOLS_FIELD_RE = re.compile(r"\.symbols\s*=\s*(?:static_cast<[^>]*>\()?\(?(?:const struct ModuleSymbol\s*\*\s*\)?)?([A-Za-z_][A-Za-z_0-9]*)") +SYMBOL_ARRAY_DECL_RE = re.compile( + r"\b(?:static\s+)?(?:const\s+)?(?:struct\s+)?ModuleSymbol\s+([A-Za-z_][A-Za-z_0-9]*)\s*\[\s*\]\s*=\s*\{" +) + + +class ExportEntry: + __slots__ = ("name", "file", "line", "kind") + + def __init__(self, name, file, line, kind): + self.name = name + self.file = file + self.line = line + self.kind = kind # "macro" | "signature" | "alias" | "literal" + + +def _find_block_end(text, start_index): + """Given the index of the opening '{' of an initializer, returns the index just past its + matching '}' (brace-depth aware, so nested { } inside the array don't end it early).""" + depth = 0 + i = start_index + n = len(text) + while i < n: + c = text[i] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + return i + 1 + i += 1 + return n + + +def collect_exports(root): + """Returns (entries, array_owner, module_file, entry_array): + - entries: every exported ModuleSymbol table entry found anywhere in the firmware tree + - array_owner: SYMBOLS array C identifier -> the Module identifier whose `.symbols` field + points at it (when found in the same file) + - module_file: Module C identifier -> the file it's defined in + - entry_array: id(entry) -> the SYMBOLS array identifier it came from + + array_owner/module_file/entry_array together let a symbol be traced back to the module that + must be *started* for it to actually resolve at runtime (see collect_active_modules).""" + entries = [] + array_owner = {} + module_file = {} + entry_array = {} # id(entry) -> array identifier + + for path in _iter_source_files(root): + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + continue + rel_path = str(path.relative_to(root)) + + for m in MODULE_STRUCT_RE.finditer(text): + module_name = m.group(1) + brace_index = text.index("{", m.end() - 1) + block_end = _find_block_end(text, brace_index) + block = text[brace_index:block_end] + field_match = SYMBOLS_FIELD_RE.search(block) + if field_match: + array_owner[field_match.group(1)] = module_name + module_file[module_name] = rel_path + + for decl in SYMBOL_ARRAY_DECL_RE.finditer(text): + array_name = decl.group(1) + brace_index = text.rindex("{", 0, decl.end()) + block_end = _find_block_end(text, brace_index) + block_text = text[brace_index:block_end] + start_line = text.count("\n", 0, brace_index) + 1 + + for line_offset, line in enumerate(block_text.splitlines()): + stripped = line.lstrip() + if stripped.startswith("//") or stripped.startswith("*") or stripped.startswith("/*"): + continue + line_no = start_line + line_offset + + for name in MACRO_RE.findall(line): + e = ExportEntry(name, rel_path, line_no, "macro") + entries.append(e) + entry_array[id(e)] = array_name + for name in SIGNATURE_RE.findall(line): + e = ExportEntry(name, rel_path, line_no, "signature") + entries.append(e) + entry_array[id(e)] = array_name + for name in ALIAS_RE.findall(line): + e = ExportEntry(name, rel_path, line_no, "alias") + entries.append(e) + entry_array[id(e)] = array_name + if "DEFINE_MODULE_SYMBOL" not in line: + for name in LITERAL_RE.findall(line): + e = ExportEntry(name, rel_path, line_no, "literal") + entries.append(e) + entry_array[id(e)] = array_name + + return entries, array_owner, module_file, entry_array + + +# --------------------------------------------------------------------------------------------- +# Figuring out which modules are actually started on the currently-built board +# --------------------------------------------------------------------------------------------- + +# Modules Tactility.cpp starts unconditionally (or near-unconditionally) on every board, in +# addition to kernel_module (always, via kernel_init) and whatever kernel_init's dts_modules[] +# argument names for the specific board that was last built. See Tactility::run() in +# Tactility/Source/Tactility.cpp - kept as a literal list here because it's a handful of fixed +# names, not something worth re-deriving by parsing that function's body every run. +ALWAYS_STARTED_MODULES = { + "kernel_module", + "c_symbols_module", + "cjson_module", + "posix_symbols_module", # gated on TT_IS_POSIX/ESP_PLATFORM - true for every real device build + "cpp_symbols_module", + "freertos_module", + "pthread_module", + "http_module", + "app_module", + "crypt_module", + "mbedtls_module", + "gps_module", + "gps_generic_module", + "gps_meshtastic_module", + "app_esp32_module", # ESP_PLATFORM branch; app_posix_module is the POSIX-host alternative + "app_posix_module", + "tactility_audio_module", + "lvgl_window_manager_module", + "lvgl_module", +} + +DTS_MODULES_ARRAY_RE = re.compile(r"\bModule\s*\*\s*const\s+dts_modules\s*\[\s*\]\s*=\s*\{") +IDENTIFIER_RE = re.compile(r"&?([A-Za-z_][A-Za-z_0-9]*)") + + +def collect_active_modules(sdk_root): + """Returns (active_module_names, board_name) describing the board that was last built into + Tactility/build, by reading its generated devicetree.c. Returns (None, None) if the + firmware hasn't been built yet (no build/ present) - the caller then skips this check + entirely rather than falsely reporting every module as inactive.""" + devicetree_c = sdk_root / "build" / "esp-idf" / "Tactility" / "Generated" / "devicetree.c" + if not devicetree_c.is_file(): + return None, None + + text = devicetree_c.read_text(encoding="utf-8", errors="replace") + + board_name = None + root_config_match = re.search(r"root_config_dt\s+\w+\s*=\s*\{\s*\"([^\"]+)\"", text) + if root_config_match: + board_name = root_config_match.group(1) + + dts_match = DTS_MODULES_ARRAY_RE.search(text) + if not dts_match: + return None, board_name + + block_end = _find_block_end(text, text.index("{", dts_match.end() - 1)) + block = text[dts_match.end() - 1 : block_end] + dts_names = {name for name in IDENTIFIER_RE.findall(block) if name != "NULL"} + + return ALWAYS_STARTED_MODULES | dts_names, board_name + + +# ELF e_machine values for the chip families Tactility targets - EM_XTENSA covers esp32/esp32s3 +# (can't tell those two apart from this field alone), EM_RISCV covers esp32c6/esp32p4. +_EM_XTENSA = 0x5E +_EM_RISCV = 0xF3 +_ELF_MACHINE_FAMILY = {_EM_XTENSA: "xtensa", _EM_RISCV: "riscv"} +_TARGET_FAMILY = { + "esp32": "xtensa", "esp32s3": "xtensa", + "esp32c6": "riscv", "esp32p4": "riscv", +} + + +def get_built_target(sdk_root): + """Returns the IDF_TARGET (e.g. 'esp32s3') Tactility/build was last configured for, or + None if it hasn't been built. The active-module cross-check above is only meaningful when the + app ELF was built for the SAME chip as this firmware build - a module active on an esp32s3 + board tells you nothing about what's active on an esp32p4 one.""" + sdkconfig_json = sdk_root / "build" / "config" / "sdkconfig.json" + if not sdkconfig_json.is_file(): + return None + try: + import json + return json.loads(sdkconfig_json.read_text(encoding="utf-8")).get("IDF_TARGET") + except (OSError, ValueError): + return None + + +def guess_elf_target_family(elf_path): + """Returns 'xtensa' or 'riscv' from the ELF header's e_machine field, or None if unrecognized.""" + with open(elf_path, "rb") as f: + header = f.read(20) + if len(header) < 20 or header[:4] != b"\x7fELF": + return None + e_machine = struct.unpack_from(" control-block constructor - does not: esp_elf.c's relocate loop falls + # back to the symbol's own local address whenever the firmware doesn't provide one (confirmed + # against esp_elf.c's `if (!addr && sym->shndx != SHN_UNDEF)` fallback, and against a real + # device: EpubReader's shared_ptr constructor loads and runs fine with no + # firmware-side export at all, because its own ELF already carries a real definition of it). + # + # Each RELA section's own sh_link names the symbol table its entries index into - not + # necessarily .dynsym, though it is for every app ELF side-loading currently produces. Resolved + # per-section rather than hardcoded so a differently-built ELF (e.g. a future toolchain/link + # config with a separate .symtab) doesn't silently misattribute relocations to the wrong table. + SHT_RELA = 4 + found_rela = False + for section in sections: + if section.get("type") != SHT_RELA: + continue + symtab = sections[section["link"]] if section["link"] < len(sections) else dynsym + if symtab is None or symtab["label"] not in (".dynsym", ".symtab"): + continue + found_rela = True + symtab_entry_count = symtab["size"] // 16 + entry_count = section["size"] // 12 # Elf32_Rela: r_offset, r_info, r_addend + for index in range(entry_count): + offset = section["off"] + index * 12 + _r_offset, r_info, _r_addend = struct.unpack_from("> 8 + if sym_index == 0 or sym_index >= symtab_entry_count: + continue + if symbol_shndx(symtab, sym_index) != 0: # SHN_UNDEF == 0: already defined locally + continue + name = symbol_name(symtab, sym_index) + if name: + referenced.add(name) + + if found_rela: + return referenced + + # Fallback for a plain (non-PIC) ELF with no relocations to walk: treat SHN_UNDEF .dynsym/ + # .symtab entries as undefined, the simpler model that held before shared-object-style apps. + undefined = set() + for section in sections: + if section["label"] not in (".dynsym", ".symtab"): + continue + string_table = sections[section["link"]] + entry_count = section["size"] // 16 + for index in range(entry_count): + offset = section["off"] + index * 16 + name_offset, _value, _size, _info, _other, shndx = struct.unpack_from( + "\" error as a") + w("truly-missing symbol. Either the app shouldn't need them on this board, or the") + w("module needs to be added to this board's device tree / Tactility::run().") + for name, owning_module in resolved_inactive: + owner_file = module_file.get(owning_module, "?") + w(f"\n {name}") + for entry in exports_by_name[name]: + w(f" exported by : {entry.file}:{entry.line}") + w(f" owning module: {owning_module} ({owner_file})") + + if missing: + w("\n" + "=" * 78) + w("MISSING SYMBOLS - the app will fail to load") + w("=" * 78) + for name in missing: + label, suggested_file, is_mangled, demangled = classify_symbol(name) + w(f"\n {name}") + if demangled: + w(f" demangled : {demangled}") + if is_mangled and not demangled: + w(" demangled : (c++filt not found on PATH - install binutils to decode this mangled name)") + w(f" looks like : {label}") + if suggested_file: + w(f" add it to : {suggested_file}") + if is_mangled: + w(" how : mangled C++ name - the identifier isn't valid C, so it can't use") + w(" DEFINE_MODULE_SYMBOL(name) directly. Use a raw literal entry instead:") + w(f' {{ "{name}", (void*)& }}') + w(" (see cpp-symbols-module/source/module.cpp for many examples, including") + w(" forcing-wrapper functions for ctors/dtors with no addressable definition)") + else: + w(f" how : DEFINE_MODULE_SYMBOL({name})") + else: + w(" add it to : (no confident match - this looks app/library-specific;") + w(" grep the app's own dependencies for where it's declared,") + w(" then add a DEFINE_MODULE_SYMBOL/literal entry to the most") + w(" relevant existing *-symbols-module or Drivers/*/source/symbols.c)") + + w("\n" + "-" * 78) + w("After adding the export(s):") + w(" 1. Rebuild the firmware (Tactility) so the new export table is compiled in.") + w(" 2. Reflash, or just re-run the app if you're side-loading the ELF.") + w(" 3. Re-run this script against the freshly-built app ELF to confirm.") + else: + w("\nAll referenced symbols resolve against an active module. 🎉" if not resolved_inactive else + "\nNo missing symbols, but see the inactive-module warnings above.") + + return "\n".join(lines) + + +# --------------------------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description="Report symbols an ELF app needs that the firmware does not export, " + "where to add them, and whether they'd even be active on the built board." + ) + parser.add_argument("elf", nargs="?", help="path to the built .app.elf") + parser.add_argument("--sdk", help="path to the Tactility checkout") + parser.add_argument("--out", help="also write the full report to this text file") + parser.add_argument( + "--list-exports", + action="store_true", + help="print every exported symbol (with source location) and exit", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="also list the symbols that do resolve", + ) + args = parser.parse_args() + + root = find_firmware_root(args.sdk) + entries, array_owner, module_file, entry_array = collect_exports(root) + + # Keep EVERY entry per name: a symbol can be exported by several modules + # (active and inactive). Collapsing to one entry misclassifies it either + # way depending on file order, so callers apply any/all owner logic. + exports_by_name = {} + for entry in entries: + exports_by_name.setdefault(entry.name, []).append(entry) + + if args.list_exports: + for name in sorted(exports_by_name): + for entry in exports_by_name[name]: + print(f"{name} ({entry.kind}, {entry.file}:{entry.line})") + return 0 + + if not args.elf: + parser.error("an ELF path is required unless --list-exports is given") + + elf_path = Path(args.elf) + if not elf_path.is_file(): + raise SystemExit(f"No such file: {elf_path}") + + active_modules, board_name = collect_active_modules(root) + + # The active-module list reflects whatever chip Tactility/build was last configured for. + # If the app ELF was built for a different chip, that list says nothing useful about it - e.g. + # an esp32p4 app checked while the firmware's last build was esp32s3. Drop the check rather + # than silently mis-reporting inactive-but-actually-fine (or vice versa) modules. + built_target = get_built_target(root) + built_family = _TARGET_FAMILY.get(built_target) if built_target else None + elf_family = guess_elf_target_family(elf_path) + target_mismatch = bool(built_family and elf_family and built_family != elf_family) + if target_mismatch: + active_modules = None + + undefined = read_undefined_symbols(elf_path) + + report = render_report( + elf_path, exports_by_name, entry_array, array_owner, module_file, + active_modules, board_name, undefined, args.verbose, + ) + if target_mismatch: + report = ( + f"NOTE: firmware was last built for {built_target} ({built_family}), but this ELF " + f"looks like a {elf_family} build - skipping the active-module check since it would " + f"be checked against the wrong board. Build Tactility for this app's target chip " + f"first if you want that check.\n\n" + report + ) + print(report) + + if args.out: + Path(args.out).write_text(report + "\n", encoding="utf-8") + print(f"\n(report written to {args.out})") + + missing = sorted(undefined - exports_by_name.keys()) + has_inactive = False + if active_modules is not None: + for name in undefined & exports_by_name.keys(): + owners = set() + unknown_owner = False + for entry in exports_by_name[name]: + array_name = entry_array.get(id(entry)) + owning_module = array_owner.get(array_name) if array_name else None + if owning_module is None: + unknown_owner = True + else: + owners.add(owning_module) + if not unknown_owner and owners and all(m not in active_modules for m in owners): + has_inactive = True + break + + return 1 if (missing or has_inactive) else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Scripts/sprite_tools/gen_bgdecor.py b/Scripts/sprite_tools/gen_bgdecor.py new file mode 100644 index 0000000..20bd03d --- /dev/null +++ b/Scripts/sprite_tools/gen_bgdecor.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +"""Generate TamaTac background decor (Decor.h + PNG previews). + +Pixel-art scenery as tiny assets: + - A8 alpha silhouettes (sun, moon, 2 clouds) shown via lv_image with + recolor + faded opa so they sit *behind* the UI + - 32x16 RGB565 grass tiles (day/night), horizontally tileable ground strip + +Art is procedural (circles/ellipses/waves with soft alpha falloff), so no +input files are needed. Also renders composed 480x270 day/night scene +previews (needs Pillow, same as sprite2c.py). + +Usage: + python gen_bgdecor.py # writes ../../Apps/TamaTac/main/Source/Decor.h + # and ../../Apps/TamaTac/decor/*.png +""" +import math +import pathlib + +ROOT = pathlib.Path(__file__).resolve().parent +OUT_H = ROOT / "../../Apps/TamaTac/main/Source/Decor.h" +OUT_IMG = ROOT / "../../Apps/TamaTac/decor" + +def clamp255(v): + return max(0, min(255, int(v))) + + +def disc(w, h, cx, cy, r): + """Soft-edged disc alpha map.""" + out = [] + for y in range(h): + for x in range(w): + d = math.hypot(x - cx, y - cy) + out.append(clamp255((r + 0.6 - d) * 160)) + return out + + +def sun(): + w = h = 24 + a = disc(w, h, 11.5, 11.5, 6.5) + # 8 stubby rays (2px thick bars) + for k in range(8): + ang = math.pi / 4 * k + horiz = (k % 2 == 0) + for t in range(8, 12): + x = int(round(11.5 + math.cos(ang) * t)) + y = int(round(11.5 + math.sin(ang) * t)) + pts = [(x, y), (x + 1, y)] if horiz else [(x, y), (x, y + 1)] + for (xx, yy) in pts: + if 0 <= xx < w and 0 <= yy < h: + a[yy * w + xx] = max(a[yy * w + xx], 220) + return w, h, a + + +def moon(): + return 20, 20, disc(20, 20, 9.5, 9.5, 8.0) + + +def cloud(w, h, blobs): + out = [0] * (w * h) + for y in range(h): + for x in range(w): + best = 0.0 + for (cx, cy, rx, ry) in blobs: + d = math.hypot((x - cx) / rx, (y - cy) / ry) + best = max(best, (1.15 - d) * 200) + out[y * w + x] = clamp255(best) + return w, h, out + + +def cloud_big(): + w, h = 40, 20 + return cloud(w, h, [(12, 13, 11, 6.5), (21, 10, 12, 8), + (30, 13, 9, 5.5)]) + + +def cloud_small(): + w, h = 28, 14 + return cloud(w, h, [(9, 9, 8, 4.5), (15, 7, 8.5, 5.5), (21, 9, 6.5, 4)]) + + +def rgb565(r, g, b): + return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) + + +def grass(night): + """32x16 tileable ground tile. Wave period 8 divides 32: seamless.""" + w, h = 32, 16 + if night: + top, mid, dirt, speck = (16, 26, 60), (12, 20, 48), (26, 20, 40), (16, 14, 30) + else: + top, mid, dirt, speck = (46, 140, 70), (34, 112, 56), (122, 88, 52), (96, 68, 40) + out = [] + for y in range(h): + for x in range(w): + edge = 3 + ((x // 4) % 2) * 2 # blocky wave, period 8 + blade = (x * 7 + 3) % 8 == 0 and y < edge + 3 + if y < edge: + c = top if (x + y) % 2 == 0 else mid + elif y == edge or blade: + c = top + elif (x * 31 + y * 57) % 11 == 0: + c = speck + else: + c = dirt if (x + y) % 2 == 0 else speck + out.append(rgb565(*c)) + return w, h, out + + +PIECES_A8 = {"decorSun": sun(), "decorMoon": moon(), + "cloudBig": cloud_big(), "cloudSmall": cloud_small()} +GRASS = {"grassDay": grass(False), "grassNight": grass(True)} + + +def fmt_bytes(name, data): + rows = [] + w = PIECES_A8[name][0] + for y in range(len(data) // w): + rows.append(", ".join(f"0x{b:02X}" for b in data[y * w:(y + 1) * w])) + body = ",\n ".join(rows) + return (f"// A8 alpha silhouette ({w}x{len(data) // w})\n" + f"constexpr uint8_t {name}[{len(data)}] = {{\n {body}\n}};") + + +def fmt_rgb(name, w, h, data): + rows = [", ".join(f"0x{v:04X}" for v in data[y * w:(y + 1) * w]) + for y in range(h)] + body = ",\n ".join(rows) + return (f"// {w}x{h} RGB565 tileable ground tile\n" + f"constexpr uint16_t {name}[{len(data)}] = {{\n {body}\n}};") + + +def dsc_a8(name, arr, w, h): + return (f"inline const lv_image_dsc_t {name}Dsc = {{\n" + f" .header = {{ .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_A8,\n" + f" .flags = 0, .w = {w}, .h = {h}, .stride = {w},\n" + f" .reserved_2 = 0 }},\n" + f" .data_size = {w * h},\n" + f" .data = {arr},\n" + f" .reserved = nullptr,\n" + f" .reserved_2 = nullptr,\n" + f"}};") + + +def dsc_rgb(name, arr, w, h): + return (f"inline const lv_image_dsc_t {name}Dsc = {{\n" + f" .header = {{ .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_RGB565,\n" + f" .flags = 0, .w = {w}, .h = {h}, .stride = {w * 2},\n" + f" .reserved_2 = 0 }},\n" + f" .data_size = {w * h * 2},\n" + f" .data = reinterpret_cast({arr}),\n" + f" .reserved = nullptr,\n" + f" .reserved_2 = nullptr,\n" + f"}};") + + +parts, dscs = [], [] +for name, (w, h, data) in PIECES_A8.items(): + parts.append(fmt_bytes(name, data)) + dscs.append(dsc_a8(name, name, w, h)) +for name, (w, h, data) in GRASS.items(): + parts.append(fmt_rgb(name, w, h, data)) + dscs.append(dsc_rgb(name, name, w, h)) + +header = f"""/** + * @file Decor.h + * @brief Background scenery for TamaTac (AUTO-GENERATED by gen_bgdecor.py). + * + * A8 alpha silhouettes (sun/moon/clouds, recolored+faded at runtime) and + * tileable RGB565 grass tiles. Do not hand-edit: + * tweak Scripts/sprite_tools/gen_bgdecor.py and re-run. + */ +#pragma once + +#include +#include + +{chr(10).join(parts)} + +{chr(10).join(dscs)} +""" +OUT_H.write_text(header) + +# --- PNG previews (composed scenes, like spritedata2png.py output) --- +try: + from PIL import Image +except ImportError: + print(f"wrote {OUT_H} (Pillow missing: previews skipped)") + raise SystemExit + +OUT_IMG.mkdir(parents=True, exist_ok=True) + +# Preview images +GRAIN = { + False: ((71, 96, 184), (22, 31, 70), (66, 86, 142)), + True: ((12, 18, 39), (9, 11, 34), (225, 225, 245)), +} + + +def grain_tile(night): + base, speck, accent = GRAIN[night] + img = Image.new("RGB", (32, 32), base) + px = img.load() + return img + + +def paste_a8(base, name, color, opacity, pos): + w, h, data = PIECES_A8[name] + layer = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + px = layer.load() + for y in range(h): + for x in range(w): + a = data[y * w + x] + px[x, y] = (*color, int(a * opacity / 255)) + base.alpha_composite(layer, pos) + + +def paste_grass(base, night, y0): + w, h, data = GRASS["grassNight" if night else "grassDay"] + tile = Image.new("RGB", (w, h)) + px = tile.load() + for y in range(h): + for x in range(w): + v = data[y * w + x] + px[x, y] = (((v >> 11) & 31) * 255 // 31, + ((v >> 5) & 63) * 255 // 63, + (v & 31) * 255 // 31) + for x in range(0, base.width, w): + base.paste(tile, (x, y0)) + + +# Recolor + fade matching the firmware styles +SUN, MOON, CLOUD = (255, 217, 74), (232, 232, 245), (255, 255, 255) +FADE = 110 # ~43% opa + +for night in (False, True): + scene = Image.new("RGBA", (480, 270), (0, 0, 0, 255)) + g = grain_tile(night) + for ty in range(0, 270, 32): + for tx in range(0, 480, 32): + scene.paste(g, (tx, ty)) + if night: + paste_a8(scene, "decorMoon", MOON, 200, (410, 22)) + else: + paste_a8(scene, "decorSun", SUN, 200, (410, 22)) + paste_a8(scene, "cloudBig", CLOUD, FADE, (24, 32)) + paste_a8(scene, "cloudSmall", CLOUD, FADE, (192, 16)) + paste_a8(scene, "cloudSmall", CLOUD, FADE - 20, (346, 44)) + paste_grass(scene, night, 270 - 16) + scene.convert("RGB").save(OUT_IMG / ("scene_night.png" if night else "scene_day.png")) + +print(f"wrote {OUT_H} + previews in {OUT_IMG}/") From 7ca1fb774ef9302c9d654f49081c05bd3fc6b497 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 28 Oct 2026 21:44:08 +1000 Subject: [PATCH 2/5] cleanup! --- Apps/GfxBenchmark/pics/phase-01.png | Bin 2900 -> 0 bytes Apps/GfxBenchmark/pics/phase-02.png | Bin 2802 -> 0 bytes Apps/GfxBenchmark/pics/phase-03.png | Bin 5420 -> 0 bytes Apps/GfxBenchmark/pics/phase-04.png | Bin 1090 -> 0 bytes Apps/GfxBenchmark/pics/phase-05.png | Bin 1151 -> 0 bytes Apps/GfxBenchmark/pics/phase-06.png | Bin 1274 -> 0 bytes Apps/GfxBenchmark/pics/phase-07.png | Bin 1674 -> 0 bytes Apps/GfxBenchmark/pics/phase-08.png | Bin 1536 -> 0 bytes Apps/GfxBenchmark/pics/phase-09.png | Bin 6295 -> 0 bytes Apps/GfxBenchmark/pics/phase-10.png | Bin 4954 -> 0 bytes Apps/GfxBenchmark/pics/phase-11.png | Bin 3269 -> 0 bytes Apps/GfxBenchmark/pics/phase-12.png | Bin 7340 -> 0 bytes Apps/GfxBenchmark/pics/phase-13.png | Bin 1796 -> 0 bytes Apps/GfxBenchmark/pics/phase-14.png | Bin 3323 -> 0 bytes Apps/GfxBenchmark/pics/phase-15.png | Bin 3678 -> 0 bytes Apps/GfxBenchmark/pics/phase-16.png | Bin 1655 -> 0 bytes Apps/GfxBenchmark/pics/results-320x240.png | Bin 4274 -> 0 bytes .../images/0_transport_selection.png | Bin 17865 -> 0 bytes Apps/MidiController/images/10_chord_mode.png | Bin 36999 -> 0 bytes Apps/MidiController/images/11_lfo_mode.png | Bin 38735 -> 0 bytes Apps/MidiController/images/12_raga_mode.png | Bin 40573 -> 0 bytes .../images/13_dimensions_mode.png | Bin 41764 -> 0 bytes Apps/MidiController/images/14_baby8_mode.png | Bin 38557 -> 0 bytes Apps/MidiController/images/15_tb3po_mode.png | Bin 35347 -> 0 bytes Apps/MidiController/images/16_euclid_mode.png | Bin 71486 -> 0 bytes Apps/MidiController/images/1_mode_selection.png | Bin 53507 -> 0 bytes Apps/MidiController/images/2_keys_mode.png | Bin 15096 -> 0 bytes Apps/MidiController/images/3_beats_mode.png | Bin 31370 -> 0 bytes Apps/MidiController/images/4_zen_mode.png | Bin 10928 -> 0 bytes Apps/MidiController/images/5_drop_mode.png | Bin 32245 -> 0 bytes Apps/MidiController/images/6_rng_mode.png | Bin 40620 -> 0 bytes Apps/MidiController/images/7_xy_mode.png | Bin 12226 -> 0 bytes Apps/MidiController/images/8_arp_mode.png | Bin 43456 -> 0 bytes Apps/MidiController/images/9_grid_mode.png | Bin 23991 -> 0 bytes 34 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Apps/GfxBenchmark/pics/phase-01.png delete mode 100644 Apps/GfxBenchmark/pics/phase-02.png delete mode 100644 Apps/GfxBenchmark/pics/phase-03.png delete mode 100644 Apps/GfxBenchmark/pics/phase-04.png delete mode 100644 Apps/GfxBenchmark/pics/phase-05.png delete mode 100644 Apps/GfxBenchmark/pics/phase-06.png delete mode 100644 Apps/GfxBenchmark/pics/phase-07.png delete mode 100644 Apps/GfxBenchmark/pics/phase-08.png delete mode 100644 Apps/GfxBenchmark/pics/phase-09.png delete mode 100644 Apps/GfxBenchmark/pics/phase-10.png delete mode 100644 Apps/GfxBenchmark/pics/phase-11.png delete mode 100644 Apps/GfxBenchmark/pics/phase-12.png delete mode 100644 Apps/GfxBenchmark/pics/phase-13.png delete mode 100644 Apps/GfxBenchmark/pics/phase-14.png delete mode 100644 Apps/GfxBenchmark/pics/phase-15.png delete mode 100644 Apps/GfxBenchmark/pics/phase-16.png delete mode 100644 Apps/GfxBenchmark/pics/results-320x240.png delete mode 100644 Apps/MidiController/images/0_transport_selection.png delete mode 100644 Apps/MidiController/images/10_chord_mode.png delete mode 100644 Apps/MidiController/images/11_lfo_mode.png delete mode 100644 Apps/MidiController/images/12_raga_mode.png delete mode 100644 Apps/MidiController/images/13_dimensions_mode.png delete mode 100644 Apps/MidiController/images/14_baby8_mode.png delete mode 100644 Apps/MidiController/images/15_tb3po_mode.png delete mode 100644 Apps/MidiController/images/16_euclid_mode.png delete mode 100644 Apps/MidiController/images/1_mode_selection.png delete mode 100644 Apps/MidiController/images/2_keys_mode.png delete mode 100644 Apps/MidiController/images/3_beats_mode.png delete mode 100644 Apps/MidiController/images/4_zen_mode.png delete mode 100644 Apps/MidiController/images/5_drop_mode.png delete mode 100644 Apps/MidiController/images/6_rng_mode.png delete mode 100644 Apps/MidiController/images/7_xy_mode.png delete mode 100644 Apps/MidiController/images/8_arp_mode.png delete mode 100644 Apps/MidiController/images/9_grid_mode.png diff --git a/Apps/GfxBenchmark/pics/phase-01.png b/Apps/GfxBenchmark/pics/phase-01.png deleted file mode 100644 index 48117d78aebf34dd3ff9cc81432665f4ba750284..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2900 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 diff --git a/Apps/GfxBenchmark/pics/phase-03.png b/Apps/GfxBenchmark/pics/phase-03.png deleted file mode 100644 index e67ec0ac9bf1eb492c9331cdb0e1961013b41fe6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5420 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 WP~?4U-XJ~M`{&Mo zss^(f6nzc|s3mX+8?F&_4s&EQlhY70-lTKX`W1Bx<+emwiW8XWr;Nt14!5 z${b-Ul1lA@T%dFHVq9jn-CeQmfw~FYX>dhA7Z-Py7JN>TT6cSrIS*b{?_XQEAkBEstlfK2UjCa&aeX|0iBh zm|&>nin(23+RH5-xGd3pmF^EtUQ9*6@Qg{4y<;=eE!({9-t-Dn-rXB_9%*v_$%bwm zSS9Pa+wRUR&gZrx3pB5~NfYGD3kuPta&*H7p&hNeV%xb%PBPQwK;u>a9RG>wq z-bSYDW^ulc54lbO26q;Bof8&CK%Y&IU^n+)cJWs4!tbh}NYByR_#kj!Bf4>5m5tfb z>z5TiR61qy#%(LB{X#5?fT_ZJP4jH^%NcKvUHC1u9vD7m(<44;+(%Dlpdj^Lb6{(4 z_~ji1f~itvi*Gf}zkrq&A>n>T_1eL+lQXxJC66XYEXi@rI{PjWd4(e7$A5uk1B0il KpUXO@geCx1nV~xX diff --git a/Apps/GfxBenchmark/pics/phase-05.png b/Apps/GfxBenchmark/pics/phase-05.png deleted file mode 100644 index c0b358086021263996c7da5a32343702488942fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1151 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoKX5Ps$$$P@HVh0bZk{fVAr*7p-o2P594O*& zQNzw>iO1jn{UI(sa;)-4_{^5PR4M*`Cd&ST<4qZh7(Y9iuurd#N7YMC`~LHEqNx1! zpPLT|vG61^wHYuv-&lTFV&DIm-`o#$=gsF+HfU7zIUt~xz#(kV(!1%%EX(=V|1ni$ z&f;S>LXzR#1(c~=e)O>H@rvxynVe9wW;7_S*?45uN5AgBd$#U6!vZoNCb^v8|FW;IR{D1A$R5gPZplYy_ zH@wx~n09XY*8L6Dv-*Dj`fiHkthafcrjO@^-Ti#+epGB46Vz^?XKw!%%06bh_G9jQ zGzaKx)GPGQW|_S|`>Pz1B?jJW%q@Q3J={J6=#N=Q-a8}oy6pa+cMqNRRb`iI3q#!m z@}}ywq&=5+&D$R4ZHVNe$fUd7)lvV8uD|m00f#BX8`C2Ai|k~}QmxkgW&NFi_xmpgO%_>%Gj%Y*RZF6T;5_1(`cF z8O63+U#)fX*Sqc=11>>Le7s zyTr@aRxqIW{aVtUt&cvUdUAX5^{%VPDNbkOy#vwOZm8k2>qx9NTRMvQRSzxJewAn3 zj+|58)^>jVv*nxk0c+$yE8Bi#R$q2Kqt0<8pKSZ9I&F7Y&I}}<1f5;B-Z{hPj>ZfI_0iWECqXB?`|p<%ad_vFVwEe*DJ`Z(W(|7TQ)KF`3U_PoyL!}+WA z3=c|~+6)+-Z|pzrV7!fcM*UVP$qy&gkG^|%+`%n_L)f5&Q{vFFcLyGq^KUD#ZqsIo zsp!*rQFkW21*B_6gQCtR=Ids{tMc5v4IVrQUiPr=iNr!hp92DF2^`t)4t(6(>X_#q zW&hw`{(;nYKhyJJ+HyK&E7q=UE519e>cDB%J$LkvSl`vqKdzC*n-ad6c z=W9dw|Gzi>aOc4_H_KKm@9ns|PHr~qo}24;FaEtzRM`aNQWl=X-)|2{zxO`9OZN6H z>j!)5f4ko4pDPJ-((O9-e|hh7R~>o!sJr3+le06hEzmu>Zl_ zYVr8P;czQAGw<)zeYZSD;GW}w_m6vP=Doi=8y-ZPndcww`(B^NqAu_ubI#G7j@-gNeIW46-@WCluSWG;03{7jx~j-5_kRBf7`p2&cz@P+Tm9J_oY;T@We=YI z*dJys_#rB0tr?Ovcb;c$HDo-#GwSVGgbfxau6F4&tt;I62st8|!nNfDK2)p=vq6d_ zn@Ouh&kKIA0g56SP{9_i9jEkwF=lPWRHOiO1@hS$;}2f#GBmwPYP+JbPzJ|%JeYL23A|hD+*sTkznbUCp$ki@wq$vO2v|2P=_`~X$wGsyq z{`jE|4W3+#kb+iJ>zWSoAO0Ea)?}!S72T#s#*&A28Htl6D z+o}YQfE#tz8&->Q-_%J6*S-c%0y)hfrKM}-fO6i(Fwbp!#P=rasM$1bpxj)zbEA{k z%eE$k|80m}djp;uHar6=WxWa%d(aIFvJEHTVvKP22bTjaUcm-bd>$T%W@$hZL50|Z aiU;;a8eVminD+PrxeT7JelF{r5}E)$1S+Kf diff --git a/Apps/GfxBenchmark/pics/phase-07.png b/Apps/GfxBenchmark/pics/phase-07.png deleted file mode 100644 index abe78200be83cb1f58083bdd3faf51b36345df42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1674 zcmY+Fe^gUt7{_-nLpIoeJ4Gf=5e8_QZpg0*C<H3X9BA5DCK&b{!s-HbB&r{E?gFKvKCm6t3K$oAaOkdEf6p&v`%3=XuU~E7nQH zOjjRQ3WdU4n;=Y~P=M8B#%LgUP9-mnrch?QyH>bnyV+&*L}U$|xG0?R#ot;RV((=;(HALZm!;8=Nm5K}E(4r6nM~phLtr=ja27k29ArYHr=8=kCkDwr1@z!dz z|Ks^@{T*g~K@$`y`Wh>Rp!D`b+r#GSpIO)oGcTJar*L%=^!??A_5O|xl64U&YULHa zv4UC>Y?rJA^_OT@AS~O=3#7~0TwOfs%{QzMv=plmPhh$dZ{P!(&8qjJ=kEntTeS~v zJRTbwEoiEvzs;}C7j-62B!bUk42&SS@_v;X3!@V5c8LpUa}l-81*X+_10Ap*h4SOk z0NDsJ-Vh8F268jHC%#CFpBkUserrTyn)=nUZ??9*IPJ=|EiM^33Tse{ z)0U;8i3JCo!VO~)0{B+8O2ZF>?Gn{Vpv9Scq5Uv$OqQZ%XrHpJdwZfPcHSiP(i46A zKHSrc1-qFq6SEqYGTkOnyp+js)5fu_JxQ(u1S=;6E;rtmL|3%lCpvYl&oh>|Hlwp_@U1pwM4fC= z&@$M_wyq%?DC>6BCWz!hlBRe+7^H>VJqC#7DT(N%5M%Sg=2V@mul2Z>TT*(mUhCzF z{bF}JJ?5e9)_H(M0AwBKA-j&{bMPK+k5M!Ama#T^njJDBsJGnKgex&t^JuPt5?NVEjk0 ziQ#5WkX)t3P|ZCNC$}>>dvlA^HC$sj-0sh2MyBT$gUqG zYOsP6{*@g(tTFj3X?7Z%8yy}7HjW)!%?$&~KGOe~B>hxry5N0{yz0LhVs#bnK*RoU w&K((6);A3e{SJ6f5cOehj3Rz41@Re~p?m73)+%w*uJM@rP zBm@G{qqsWLAP`^+$fIy5sPkSHju41W1I2mkuK1kc9;@>&w_|wBwK4tK1=AjZDi4}w zP1%vOj+!$8i-A8FRmL@e2kU3=U4C2q7aSh;q&7EbKsT-iEcAJKiK-v5G}qO-%7l(3 zV}J}ZuI;_f#mk2d*q|s1LjuEfoLzzCp*R6Xq%b;rx7535dm)tf;iVsTMJh#jiI^pu zn*I^pH(l%G?tm`#AJ z(#D14$E+{O-d&5#OUM|vc%lnHM(7s_nEZZ4Mu2~d;y&C4)nh96lC*fFX{}MXTSzYn zsdbpZsf6l1+Nj0(DvUVKH@6RytO`P4k{5N7U|j@8 z<5Id59azrATPmIGdTsbd^3|P;e80S|A)WE_A*V>NPoqRe2QN8wTz!u6%uYVamZh;^0-P8F0!`eL0k;j5YJ_=mkr(uiVzc?XMk;1@X1~l#h3Q0r6|FpI<=pSv9YJh$-wk4O6o?1i=^!@b zfVj*(pq$I%*Qd0UGEevySj1{OU)^=3{GbepUmuk3hWvXiw#4 zw~%lGkrB{-oV&b@R78sofDY_}9`KarIE&@0loRgrHZjq4a8;wMD@4wznH#V3Ic0ZS zEDTAt>neF=LAum_J_Hrsn#+|JFn^(`{(-qup_}t~kz{cYoNo*3KFhOr6XOv4d{}oD zFCi!Ku0ag7(xbvLZZc&K6+Xt4I~=utU{LwYMY1Q7&k=}z1}2sQCPhr&V$o)x(oM3= zx(PSvfoF`MW6*lPlEwZot(DX(v*f$+nOa7;oR3CKG-YnOJo(nT;Z72iE2p>EeK|I=A^JS&}L zZ+^=(o`N@y^#o1BP<&ApXd*IW!WVWK2y+o=Mi^h{mtYQpbRFd#A-z=nfEUcY_eJ;PC~4v*4kFaG}R!Y|sAa zx?NAM8hOPA^~O26mi<;yJyK@YnyFa1DwlA2*u-7(?09hpl|xnOqnoPolMB_lr{9$L z)VFCCImbQ~u-?xB52}OHZw8i>*t2QsM5_w6O8@K63y~Cx?*W;>r)}A#J^2e>ii@Xn I*)~S@d3! z%9_eB7!0x7YWrRoOl}Ks?pB%w>|?g8&0w&_&bzm7-4~k7ciQ0ZWUTg8c(vybMvRV2 zv(34X*eav>CoY!7M>EddIh@ZZKl_%nNcBVknk!bd^`zHyvwq_%SzhSE+5d*R+6iozjXM*J57`RB^ib% zI@B>fU+RAbH)1mDH5Cb-kl^W-d$xMfJB(@AUD=y|0bV^Nm|c~FA-1#_7@=yhr+%e8 zw4pq-ri5Ej!uL?v>RHZeS==pH&%ssi9g)U=h5P7c93=R){NA1&mwcQ-yZT#BvC;7t zoD}F3T*Kh7)d^b+FTE?3W9nnziu=kpA(q-9KL2uT>Scq~oK1H$EB`(pL=PC!-!>uc08dZjvsq0P2|P71yyP4$?L29Ey~Jx*uv z*dZu5}CSMYkv_eWb@_YWyf)qq}6J5M!5h7k<#9^8gYH*XIZ{c)s~ zQGN9G)GK#F0`z!|7^I*PO-t9gxqOYkOMgRaV%liYgI3QHznpI$2uVH>Q?Ix`k3Zf@ zV@A$)_4{etoF`AN04LYyrBQkMHO39!cC7R?Ey-;0ZCi9BnLl#(T0zR8eaDOXF8*2* zJT``fUfQ8>hvv?0e2TW)cZNQwa6$RlqNXkOa-Ge{pH`PTorLe0eg7iiP9`j*2eDw` z$90b*@49s~Ti#Jxt2DA*iX;DDQwwVgN7*|e=KKrr4Ml?DwzOV&_Tk3FI`TR~46Pd}IYaxqF0c$7b0lffVRKTivQ0z!-Mr6hWXoC28eLUm6I3p5Hc1>28 zH}w+fB?p4LjOvfZ_@p0F)u_B3G7ZQ44|xNYM#%ngX8C&130~Ah@J02D2&8XKJV9?j zy!~?~td6!9p;x$p$6ck5ikGZ4=>$_+w|kR<%~u zK~(otM(XfgGwYvK6g%UT{lqtSx&2`&%2Y{e~Zn~r0IebtAW z^{Yk9o)(fJRx{Np>lGBf!{V&z;BE-+(Xor3$|dw|a6j@xASE%){2BS1)I)Z&XD!t` zg16Lq8BSFrX6_oLc4We>yN({t7yNIBt$KK*_1>48x5XFXJ_(}Ru4M_IKIvj&V&gY0 z(c0X6ICSv*b@2sv-o$P1dqJ#HoFK0{M(XPJ$agjOEpRob)V@%D()&e-TqPn@p?_iC$|*?vJHPLPZf z{EiC(gkwu#t7ZACWv#ojdQ`hUYD~o}dQ4@+N2p?U7R0dSB>c|uHY9Ae1gZVov%!~n zC34G^p1c?w%&nN0oEemq8FVXCa3xcakV!*%Ggf#r!U`ig=bE{VjIhcx9w?@CtWfHoFJ*9%N=zkQ*w< z^a^rp1^HP8*__H-N#!B-1;wO^MG4`^Wj`0&w2^ZF@@iXSL$-O3f^=buElP_kJOdJY z8CQ(VpuJwNH0Sdxy0#>ak_rvJ4#`xmp$lE4J}g6*Q((=`*a4Z8LTn0gku6QgqxHyf zt+qI)fD$}eqpJuOwwDVNZhMgj2uu4OvE4LUIHJIemp1e8|%(0v5~j$ZXfJufn(mGITiw zR@c6xvqdJg3+WwOU?N4BlcaK!MK1k2gckeWDo`smE8K<>j-iADX;9CBgL3dAVbLV) zBD))7D!yBVidx%*ieogPV(Ls`PLfKz>j2ucz6qu(?|S6qNQj#vrtYCl-9ziTaI3oT zJ#}n#nzI^>YlgQ7QPA2DG4?sJwSGUCTx%ghj{y#K1~KJ=BISUO+>aouOMO!Ez|aaM zR~cIpGx1B|J4|p8TNjl^H4e++{pQB>b7MNWF)iJg?rzLfoZuQxpg=Y$w|yXiqhz@4 z?4&?3H?Wh>N)4T9%4zy!@5Dp7cNe80?j8QiqUdu0tsGg27GjoO1!UX&4`=uT$Oh6W z8M+i_+fLTIAEDUGVsx46%a$hO(GCQp)jh{^Yd{J9QlS?86!A*7G$D@?pF&#g>n#9+ zA_cMIDBw$wOE5BOR2s`nv=FmRC1fz}F67C#UyO|W-vaG8xR;zxDowLA_^Ch&Uz*hl z*-_fTCu!pxiX!U{DNK{4ggv>MBBu@!6+I+nDPhlIxX7uGM*>E5wz)%c1O+*L zJ-Y3SQ)vKJdqI9wp4pIa=Y$i#lWp_D3Zq1^H`6riwDK}3!kmn@gc^}cztY*|K7Sgk z)M6QW4(2Ea^OX)x>RFxYSw$7(qzdw!%wL6yMR$a7m2@Fo$QHtDX9{yN+M3({4BE7V z08@&}+qiiQMTMihIeOllmEPtnyv_Bz`6T>$5}u%yon{J;Uu+?aunFvdTBavRs4Yk+ zB+@RBP=D}|00DcMhN;Jx@>@uJF zQo?sr!r|DIPtv;Pq(VbNz--H98xI8g*gpDR--Pt(r@Iykyt*uYrPtc8KyljC*C{TT zGZmW7ISlpD(*EH%e#PUd?Q)7P#YHrv-Rr7<2z{$&J9l7Ng4CudE}#7|P#4vEu-_Au zpr+doPns1xq&7`)`D8RhT{P_50U#){VtyKRmQb{(sC*7N@jn^9O^$6NJF91<0zYb5IV9`=3G3*_v~gq3-gi$3 zcYH03j0M(dzA(~rvSCgNb0XT7rDom$>gTL}G)#UgZi%)qsXkJj=C4lcRHJDDKk76f z_ukr`jgDv4rbO<#ly;i_FXR4&;4sJloc^FdLLre3fLx-%M>Kd^{|t}v%}5FE-7b6M zc+m8~(Cd`a|Gdi67n7Pb1O)UXT+l6?;0jKVfTN)t7%LnYk>r~0@zKvDj*{~sx+yd% z;wKa7G!$1x(G=^m=&A-ZA)4PF{JBtL2&ikxVN(jGWTFL!La+Vvq4AK*@MvYQThsSO zK@hBTL7^2mC5j8`8lES;X^P7yu^jr1>y23kpacjTQwpYJqJ@S+1G!N9JZDliTY0u&qQ2`y}MoOugllJ7F5f#w&B~GW$zaEz(D9GuH zpoe6Y8v$#GLRr2heyrr`gcGHXg2svT)ykfj67ewHv~9a&%z_|NlL|y*qS2iGZcVS6 z@<41Z30q6T+LEyEE6BqYed(*h=m=T$^@60=D+s1S{o&Dy+^PcB_IsZ?;_jjJ>iQkE_TP&+C zi$EY2+i$gYMj+<>1mz<#643W=$2DjKLTQD)^_JZyGFc=e4AS9;n0^nxu?DbK22+ep zSnmnMRLs9`OIR2a1Nx|Tg`Bo&mrzP_p*S_lfU9>orKc1aU{4zCS+ZC6rmaJ-$EL)m z$%lD^bj#^BCtMk5(1NvNFn?undmQiRUg<_IL?1bLH+}bxDl}M;7@n zx@J^tlu4t~K5c}kb*f^#J-sV-XD3at*A@L{sn9JeDj6|u%(pjmTX8~DYU#f6{nqEg zFWDRum9#E+cA(?%64FWh=H+|wOE!oPic9xZI=x41jNh72sJgEBX4=7q=lW{5^DBM`yc|}VZa9ezqiynJm&!a*LvLGUxJ>Wio zuGiui9J3>;Ax+Y^5Y@;Q>DjrhTPfMCTYXG4jxftop7^4&uPV9vqPha>n-)@XCa5?Q zt&Pcjl96(#DMqF0w3nlP#H)LEJqhOYHhpp$JGXf)qaOERD4NylmUyKF`y|=f(BJTy zduJ;abN6x!)+(+LVK!3GL7nz8D`tgPb5U(r7k>caIh7S2MA%#m2Koa!WV#mtEC%Jt z^i2%EAOh(E_6h*A%DN*UdYQ*1BKF|CC*6JmqAMc<1Vo#`w|sEWGJ-0R`2ap{rI_;h z#~2fhG`G!$v$o0%*d?mxnp9f^ylza2PU?!jc8lS6hVUS}#mJhTW)awaK{lYiFRt!JQ72&Vg2Nd6$|s*wAf@aTIJPQvHir3l z6Cr@T2X;L|Lp;Yr&rdw3D4oxV)M?1&hm`UgyDXjX;ITZ6=H?qjh-aD6GtUYx%HV%D zPR8c9Pu7>>RZz*VVxN>vISpGyn50)*5!Fo64={;psEnjBjf|vYwq+?~3ys0Vt4c>v z*|ArZhRA5rh<=7brX4$)?Q1bop7nr2N*Fcd-VW_&-IXJ!g%yHc1ar%6kXkd#hczLD zYv0GSdZ%CDZi=ITP)fV=Lu?UtNX)9<3DcjJ7Zn82Up$-?>p?Z1`3x zw2Y8OQq9ruF)xgd`|BS+mvDOG=*~}QyM@j`zAPa`1iv#tS0n}rMD2F-8EsaE`FNjD z8*I!|DN;l$oROWCVLsmPBnFBD%i=H=;AMGzFiu^3bQXj(G5tct2H|Bh5oOijD8j!p z{C+g_oRGgm$RtR}2fj_n5kess!9pI+fQ9T13z^~v3mLxUQwc2vLWYl9J2%V;d6o)e zPRQROWDiKl9^WQpTn8-VBB7A0U?F3LLT1834ufx{LQ8>=-Go98pA+(2h5SEM$oY_v zy}wP!X+j~F!a_C_R>+5hLiUG+4Bzspgq8v!!^f>2a~1MzE#I7wzeC8wEGdf;jTUZ| zCt-O2piiblWpX{YsvLw0Ycn!kiQ!iQVq37%3?_+nryRuC15bYu0E=V5hV8xw`*;Y( znFlZ1-@(h$AR)%^_PcTZKM+L}%qBiBi$4x9!s=>;ql_>l?LWQUOl{2X{<@9t!9$E2 zRvPU718yrKHEB!#5|MNr%G*xFR#=Uo7p>vfzk+5?WFnU(=>I0h@Tgm3E02ngQvl5Y z0P4iX@cvy;Ph?~{ODXZOHs%-03@1rBBlCZaCBEqIK29$9om=Ql=6J#OaNhB%uY8UO z<{KKkIPIDmU1%nfD1j7_^gvCXD%9j9TjcM^fu1)rjFfy+ZK}VNG762%5D0r4jCGNf H=jnd{;ol%1 diff --git a/Apps/GfxBenchmark/pics/phase-11.png b/Apps/GfxBenchmark/pics/phase-11.png deleted file mode 100644 index 69a8cbb6f2ec3a5aa197dcc1e0191cbdc024ca62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3269 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 D;0!1@ diff --git a/Apps/GfxBenchmark/pics/phase-12.png b/Apps/GfxBenchmark/pics/phase-12.png deleted file mode 100644 index b94d50722200a40c43252450ceacecb39dac7fc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7340 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 JS_SKf{{zj*Yb5{x diff --git a/Apps/GfxBenchmark/pics/phase-13.png b/Apps/GfxBenchmark/pics/phase-13.png deleted file mode 100644 index 498e17df2a49f1f3b53722d9737e2b931bcdaa76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1796 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 diff --git a/Apps/GfxBenchmark/pics/phase-15.png b/Apps/GfxBenchmark/pics/phase-15.png deleted file mode 100644 index 25525b1e7d11eb3279bc1064f4889ada8e3a8481..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3678 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 diff --git a/Apps/GfxBenchmark/pics/phase-16.png b/Apps/GfxBenchmark/pics/phase-16.png deleted file mode 100644 index 3180c2c9c169c1d1b0457c48e569b4966794cd6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1655 zcmah}Ydn)}9DX*7Cd#B78&Y(nC{`^Z$B15v4h|(rIZlMB$tX>7XeDYa_IjtI(!q?` zqba@ZjhvF$<4H6n%wg7K>)raj-`)@1AMW4(`rp_8x_rdI^?WU=2pbh|lriZ(; zF90BRC~DDsq~ezQtic0-s=bG^Q=U%F+%(xQzQ;S7U_eVPa5s$S#((W?TF47z!3Ut5+ieMJ2>xle@XDtmK>Tyc^(uS9zSK^FV+har%m7H?;EZb*SRE}4YCaRCT}cc@O~ss zBi=T}2U}4D4J^6t>kUr2_xf)h;Ow1P2%)o>>7G*3LDET@`3j$~l37lgJ7njbYGG7_ zVP6a?J`<+sYtkA>#--UwXBgVKuT$atT28M`B)`s|Y325GUGTKZL|9G(UNo|*7v4Qy zP%|}j&V^`#z~1QEPT-vW9y;k~mpnr9NHj+F5j5;q#g0A;GfLt63eE$Iuo;q*oU%Kk zqlVl(b1?mmI8`unI6?5Otded9Vjj=#`&sgjnZ(;uEI=iLdi8wpP2QuKAOmhJWunh{ z_`M19iyOw$G&@ei!IfAnuIZpcqetio1vXmo|11vgX!y{f=#6v_hzAvc&Eh5!dz}Ps(6!79P^3Z{0qVJjjW%U z#OV9IxYph~hn4Q2bJr#=taufd{-|~&hzi9n!Czi}oE1FrOuiR5IwiU1bo|5Tjk;er z89>sTxcm|?^w$95xEUyE=gHj16<=4=A9Ey1v|vcz67OQX!NGIfTW-yZ;a6v}=N_sW z-p4Z1jgJ)C7f#!Iol^^A4eH25bL%**T5$81k3x@eD(d@c>$y&b5XoSoMMWCAH_MHS z=oJzz-C-YahBsP5c z>u$*+8Djw=cX^vmG%0uIIn#}Z3r#3ozK-7$=@uPj&7vMCyk)nvJQV})C;PO07Dk{H z5m6?Chu-FLldhr-W8Z}Yku;_-uw{$QddJvx&Pk>qW*)8lP3uOqGSP~>t1UU1ZR?OV z6l-=qn4C*J5_%)*L_%imX~(nyl`#VwCq=>)Yv4a}&E_AL5r%6F7sD^j`{wO-Lk9Lt zPRiAtf^6eEpVZgOwpdkTPn+_CYrl%GyHp4#y1HkpVU8dxB{rhao( z&2X^qECY&;+Ih^zfE7O?7uP)@EJ5M}5*+%-dO{1(##O?*Pa%XO@v(|lwVrSV_}ERt z^TB+Iy65B;LWb&|pShY5aA&=9O}Z1{+j#ArB-Z*dx~-U51m5)Ucb|`!$hxF k%MRnMf4|iK$Dox?7?YFscbi7EFH=5xxOh81a5_x>7drzEj{pDw diff --git a/Apps/GfxBenchmark/pics/results-320x240.png b/Apps/GfxBenchmark/pics/results-320x240.png deleted file mode 100644 index cc23ea189c66d330c8626185d8457ae76354e64b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4274 zcmaJ_XH?VAw*4iM01^lm5L67HNN-X?072=9g3^*uB!Zwc>77Udh$2O)0s{U~K}l#v z=>nlil}HH?LsObG5s)VB#`oTRAKrTFewj6E=FFTuXPtG<-V=M>)R6n|$-@8ua2p%x znF9dm5^Mg81I!xt&7?E|fH%Zg@3LiZ_EJvcy|c)pF?1r>=n_<5+{KGAIaLvoich)7 zuEp&;hGZCa_-tgxg?L!PZ8*M&*|CL%Lnhk z1jX%{5ePuo7RC{Xm#F~c{Y~!n{VsX^o!|RyxILTCm58+vL%!Lg``lh3yDGm}7D@>L zCv0vSPV*_>E2V$zUU4}2Dt^tT&8ratfpz~;FUeUUyWYtSV&Jg@yJNK zJZbnA)8q+-%GBY&YrVD<)w1rO40hfKJH*G+a;gb+i8}l0M}6vUYP$tqy0_rKf7dNt zLPv=AP`L5Qzyaw6js8*pw}{AO3SiWbH^nrFtjS*Dlb>lzors!@XJIBpaL}Fd%Gk# zYMRWHQ7+a^gRe#g_@bxYV;4B^n$0ft<>5DU^dR~KI|xwv#b?Q#Clr@0_2zIQJCq=H zd|f3%_QPrk$zdTMgcC8Rv^@j^FfU8zO_sFw7GBU3K@%6M4mKk?!Pb4_ z_bLm^m`lDWp-!(jen5i4yy#hZDkS4@M+Q9qhD!L?@a|9RZuuvV>%EOj1ppqSNSI6vmOwPj@>h_%xFp&P*Z0<%s?RQ5a5})0UQYs#6 z7)u`0-23VQ0JGDsYn!X5G1Xt@em=xK*?CA?=>(8n``;Q06-M!vfv(1MO8(##F11)* z6>v7V(A=uT%FjlBmddi6OMy?L=f#16vi<&N?$HJpqJDoW)EwP=dU#tJ3niS#=Id9J z2q85)0^E7h;{7Fjt>Eqgwb8UdK7}0-e_QHcxfI-^A!|JhWT(BO%ckB5^Zg}IDABY? zT#}Wu`dQ<{?d`q@_p(L>ku2H7qvbdR04QGm_kuqAbGMrCWaZI7Wk%?ywDs z3*~0Kt90RXJ1_M5u$@Eg;jbB9(p=K9f=9iYRF_j{{3FO;hKP=tn%RzxRwioDVdYG+?d7ZpHa`owy6(H7}@cv@= zx-1E;f>O9||I3E4Xc{KoaZR4vdvwuJFfs|K)up0_&}`ks)*|WJ{-ha%hTojp{Z33lR(!JbmI2Bl(qH<%*H1DWIMrnk znnX_ZPj=>ipN6bqyN;0@Y{_0MSFpV>tC-C9#=L4k^9r>6wxkQ*Ce4OhV=Oq4AZwl9 zDUO(y0d$q<0U5IQsUBF3dgZo`dY0~c(5QD#H@IiUg@s$MmKQ3`#M3^8d5t_k@m(XZnsB?cMV%0lzvu;-kG2=#Qx|L(E65Zoy zk+sOy3zkTAbayViO?K1AMNG6g^uKX2W}6NG{bO(~pJe*(zEb8l^qfvU!qj;oGtPhx z2mA|zLO1b!Wv|A3IhM@sM*U+8#929QeAp%%2`wuo_m!P(eL1OL16+Pwf-xQl8k(M^ zVw($}CTvT)CK(KHl|`RM*@`_|D6Mx#g9#$5M29!E_FTo73^v}ps)8#n9Br#a?cYmq z7|qTP-vknk>_4i!7NoZxx(x0B@cEY^Z7S<3sOF>42%gyg3CY=JqnSRU{cJNsE-lzr&umk9*vcrU^IEjvZHV#KLR?lHiW%AEzfx!*ACx(L~&`Gj%7I z9teXHM19-7I7XjFFL!&K({U$=4Oib=%g3rF6pBoQtSH=2rY56rV=I`~{v3Dm-7~NK zdXH+F-OUUH9^bB%R0``ytC-05yRBu}R$=j(y==foF?yR33(wA|SD~io0so>La?X>( zdwnse!Hfw{^l8|+$ZE7{;S-}?*!z@`L%m=O<(Hd5{*>#Eh~pdsvBjwU8Ps5me9n_L z$p$=F4!iRxVsM`-U!unMI5)&#;%^H`2u+Fl>};ikw<25cm#verDT!*mvX`P|^u;x; z)i-tT%HmM$Jn;oixiIg^s{um%Wc?ra&l)~{=~rEeq84v6@%AOEnu!<^jR?ZY@MwC= zA_Rb$8+OA*=3Ewy3}p;`u7}m;1-FbN??cVs_w?-%J8XepKG~#~1;aBZ=Qgg%P$EEy z`ieNy$5xr})`|p=*N$4kj0T{s3EEt4?5j}hz=d%>@<+i8iZQw^zpP(6pg1c%1%psy zSv3sWv}(Ib1VNj_;#0fYY@E|9gkL9#*?FH`G%0L|{kU2k%qsO`rY;%J;HN>oO~vm@ z-@dErypZN86rt#{Q=BTbgBFACpqjspAD=j!DMjjv^k3z7>oD&1*U5xrH;_VlZFba0R+8*|I^~UVL zfHg6E!tR^C+5<1AX1VCP0(jn$gf&7wP^#&=}AP4ayG0&^LxT7wyYFQD9aw4SRKqa8)r=Bw}G1 zBI6fx5uP1AbV^g>)QP}#h>*9iH#xJ)2ndhBQWh~cUWujx3f*j061lRJE z`A_l728y(W(?!t0gT6PGIH?omo~*UTJtAUN+?HZ*mGZDNv#VrR32eum;*+6LRN+1z zdM5bmm^0D&NfsAFn;Gem0e3&5(yKCZzJCMXo77|NrHH5!Mz5#I>QOhrS9RVdI?~am z)9Z!em3<~_2kd3OIi!HrL_fKamaI2S&AGH5l%QvPLh?6*VeK$eK9Qi$3D1AD8378s zR`5j`CsXNnm|56_g{|{6=CNSYc|`Dg2}DL(GH}h~j8}`}+SZe7xLwh88OX>5TO_iK zF+!8m^wm;ev90B{j&08Jk2;JNGncn9Uwj_D1=S2lOAmLH^|z=lkKO!WG*r?AAe&0$ zhFm@iF9_CxW}o4(qeq<}0~_lBln9}9)it8S3y7DsQ0X>6cz+!qf35)_biLUV-~{CO z|E&QA$)&Srz(6+}{Qro8zd9<40D?k65Eu;n|EPxlXi#(zeo7gL=lm;@{w*{9R z+7O7kKfI6;9p?{HrLH_=ysRhXDO;yxTa}VrX=9dNKG7ru!TlGNU$DMF=d)FhZa{L0 zQ28b56Y^wKqib_9hZo;bp`~Xw$q3~rPE%O;@aDY z+E3W2;MJ!T#mWQ>aBmV-_Jztg_%jV8pPznfj&cN5_2#EyRr>@Bn&sk_#BD{p`}6v% zPrCVZ1Ct>Wj%ry>Lj`RX*F4tjBvfTY$@$j4(S44#t@KBwG33K}#jQOG??*Eqs9&Ug zv0CZFnW^h0w2p&uTMth28%|kUdd=*mzRCqD-Kn|{vNdyFwr8)ymhcZfpIpjOU@jX5 z_MKLuim~V(fhdxP=%KyYdC@m3VP8DXO5<2 v;Wx+Y_NU;i>JN>1j``o*1%RR+>H(1?#p!zGd$CY1#QTO>M?z)`ex& zLqDcEdIm%_P1StDEHf(I>jz$s{)*XxGq4LPI`}yhx3UY%n8f9VEgX9e{L;$(_B`eb zvs)^;Nf@TGk4R{-QP~h1r=YB|mcF@-y?3BbXhdk@$5-_e%9;jlL2vnFHQqtbdHi!s zZJixE7YVe285p_T0z=GfoK!Rn6kceFiOUL!%LYZp=^DS1lv30%Fb|GS(9korb@w&2 zuqD@p*e8@TTYP98nTNyS`zPnZ&hawxYDN}zPCh~YAEKW40wR zrPucl&#$3lKS1SshHU176rKex=s+W*LuKGXc7lLYD?zm*C>mxc(rUoi0qENjQa}u_ z3@0L?!zCbNU=osirY9t##>OE@PQ|35>1<{7USHpvhfiMlg$*q|m#Jy6tehDtk2tc8 zFF2zXRMLh0fP#>cf&3{i1*5gSHN2udO&Mb*bM2(G4hFUcOjwv9e=b~Z}#=<2g z4in5(lfGK+d2%5%=h;X%E~K;hex5%B_(Bz$f#I)E(LZW4Qe)NCO#D%eP6j3 zc06LbQVLdr&y38>-zcj)i^-T98V9Otdk~Y*KceTxC!nCDV$sxek$k4l$RsQ*qR!4C zB`0U5ukXXlr|{y1O(Y?Y82~&dQjmV80`KMxB?{eJd@1z*@E7cL@uz~~^1NoN z)MPPl3Mq8s!07Ui1y&*?)UOCK=h6?t^WOT{9rC}$Z{7*uLD$Si$3U9s=wuY5<(}h) zqkl+mcSNW2$lsQIZTdPp2foUgHj3Gm>_ASlqu;JJxNL46D{@65ZQcZ!#+Z@9_;@vD3BRVfPJ{?`iDCPM%V> z_slpB)HONWHNR?AQDml<;zejRqh6D#i%tm`%^Xcle;|@<_(>zd2AqAE%KS9+^qt_G zbr3t_6cPkIC)6r`Oh_lSh1ftvQHRNp1LEXwg9vMT?nUlhx`I3a5J=7BN_WP0Buf6w zQa`x11ZsH2Yq*C{5NmhirsCO(SC>OUwbxFCqSk=q+cCa@Pn(Df9#>9$hGBP)euu)Y z?|!$7HfD$CFwi?yGod{t^*fCKosM7(h}Z1wnh;nLn*7~jv) zBP`S&&_<DXEZ(|Obw zEN2@cAwpnlu3i$ei=p5byL$q+s_A0t@N*vTK$Y5(t&=jvzTk*99ZMQfxHYQEo2&Hh zu3R=f8-0!6{EThfnnMn_Iw3RONj-agdtQlkC6ES-P`#?!qLEf-I|mmMNCm5uQk-3* z!&k3_pn6FvenaQN&9T8Xa|Bt(7xBw_4e{yoU(fU&rOBd?8|Yr&)h-1Ze4kRDac48_ zxRdzw^0cs;g5~YlRx$!#k6`$+_4Nli4}JcvDUsd+k~3%iH13OosrB}1)p)!2b4eD| zrqbo5-Rdp3wYklYM_t%ql~(T;cy-Wos>?Nn<3ujueGKsA%v7~z_K&qgvwdAyNIq9_ z)zG29b?nY;zwhM$^Mn*zS20prrBgJYpx_-li_({IumqCI2n0Q*e853l8 zjJ&e~1@Lp0$97ONJdkGTR^B?5)j!?m;V+lEfW3c66-Yz3l2{FM<;bz*eX>p9dfTuG z7}Q}rft+9hBAF19kM(&sul3&T%yu9H(p!Uud1MHdQhS!9+@*#+X)oC+$ahXroa7!@@1evl2}>n-^FO<(h~EZEI3xMo2Qos!0xn11 zr}R&qvXr(z06bs67*Xi%DB=dD9B7Zzsey9ribrhWk2nm`LLJq;T97+mx`7LkC0iaf z1-wJ8KW{qptd`C5~mbTGTwAp(G)60OfQ=F7Bn?W9mKA*^vFs#~Dlojuk=6F5>JMPxJAYNwNG(JZJ&p4yy zNQf)uwe zWD9=7dOh*z?U!cI-B*~1`2DdG5R`Cxx&H$wyAd$$r0m?|5y(f$)>(Osz=(*y+1Q(z z;XT&yI53i?UUo+e5eB4-Di=I{Y?>BA2>n{bx9BXKF+17^L3hHF9RiVJl&8c^!3Kiz z2ut0RV%Ib3kAadq9@dKRW)Sb`>mH=**6FuK^R9o)b<3{mxFb#ifruJuyZ6NKiK&=G z+voC91H72gjyxD4Xp#F-J|7k(TJ+Ab^Z>?{A8#otuBO3b1bJtBxxbXqApA_n8YgV8 z^#l6ky!rDW|L_h71shw8-y+IKQ$C7XQ+FcU`oD%n)1^2aJ4FuV@XQkwMJi zk>o9%lwdA2&VvUo5$scFVrC8om|l-h4{eHqYnUwXWLu^6u5{67TRH%>SOSzJ4jcd~ z#UO)m0}#D`FsfeSpAq0Jo}(9o>@pJRX1T%9>CXEQc*I+1=utD&f!ul35ddpyv-~@1 zzEKgsL5!MP@B6@_;J>40OcBlvf-b?HKroDi@Ew8jlh4|ZSmjXY5!dhA?Cp=L z8h(iNb68rN_1-{H3c^bxWSkPt3a_d{MdOTV2(zF%+J_i^HaF#O%>6PKf%&;b#ib*o8+r#pGFB;Si>IN;21?(AvBVjgZt z4{Z1IBy(zQvL%C@v(wJj(4OVoPm?JT z&>Nfj2`}@_CHQgsQGiHYT4)097gmuLbNI*!8Jcz_GJ0NIy;sWM0#q@IAR+qHudPKL zzO+slU21}k-)}TeMh_9Ob^7($X{oY?;_Q$$xZUtv7$R@5U#r&~98nA<7qb)N&^z2B zIrW|#$}*|+qmX1xV`59$TXs`S(~a8Mz!4FG<=JR{( zD$xbrPJL3c&Ew$?&WK9fFt7|OZ}$<>ftj?&N@OcWc`35=VC=$?PKIWqA$Gkp}hO%fLq4B+FS;{&-UAh2XD ztt9ZC7#STHe*=JM4e)6JX)G`@#O4_Uz-RlvKxtVj-pV)msh=_9AdVLoG<^|jvz)K5 zt!t@S#^QLb@Wnj?*UsnG2unuav(0z>anD6~)lgnZ#bvF6I5(5HFc(V{%|gMXLZtsg z4`JnN?Bb>22?^Ja6ru#kSGZrsHRkjkdU3p}7t*0n8~GAX$Eq3JHHP&O&)1y>&ck*1 zLWd6!MDt z%=LQWiPgC^$SQ5!_uC<+3+rP1ffI&HS#8TI3n;oxSNqA(%aifN5(87nPo1BdL(GiCR z-AuPon%f>B+io2WGxS={k4hwUvBT zv8ejimR>vg(41jc>M4>hf~ z+dr7O(@1%I{$ZS&u`&8sK<{cS2MS}a;X{7Y)Y9yPrluuBHw1`z2Rhr1Cvm;WBO?eH zIgmi&Szoc6pD0XBzTXi5*^wdM+Tq7xFP%04O7L>g#OowLTR5}Xzrn7t2GcLOEnk5F z>taTuXr`j!J}9!O?S;tCCIP6WD*BNnZRe&%w|W38{As6KOX@|SssHFEnDy7yD(RFe z-as9Y!P5RPyXNDVf_~Cf#&__UFhTD++*eC`=#w;Ao}fO8Kc zS{N+A&2aPI*2^&DiQu?R+cYq{3+muHl3XHRRlC-<3$IE+J|unA_+jiO2Qe6_-LVIS zd&uo_xa(U;sD|A}oS%wU)EF%r(?O) zs5@_7gkXV6f^IbDHx;l2kZneI(Topt`9$hsKTmDZC=F9wCyux$UF-1q7ZXbOgGHmq zO$^Q$uFEK5{Dp^cGSR^SXvG38J*{U60M@m7y0+*;v%`-vl$?u%+0at~(2YlFqT2_l zIaIq~_?hnIki99SkE@<=I5t9Y-O%gQHl)ErN9p z6(O<&18$nGs*p#ivM}1tAkTQFg+{UVkzaBxt#~LtM0tHH6?mr#`!B-8GB*EoGCEW% zH?~^B1DXR!@DIwY>sPaYN&@ObMW=U?fpVS>VzYfC`j=@L62U_&`vM&$LPZ|*olsJV zyM`3i`hYE0)y+SJeJC|#Sbs*6Rn&y7~C|`Nm@j&7Ue-pHu^c~IPTu< zjv4B>sX+XuSEB(5i&_tD|8!MHU0i?R(Ldl&G)aUlV{tBo=h!^ur%*vD>gEi+HZ3&- zQ|=9PV7M&lhdajCEkwchSQsSp+N)*eA=-=b0a zxkuceiRt4Z7@nWew2WLN%q97=#6!mnYX(eoY561icrYLEYzykw1ru3D4S!+ZAX6G5 zO{@j8e$Ui~1b;Z@$@*+)>^(Ul<8&mAzj8Vd689hq`(#ZHL~cJh&%teX2&)6~lf*Bn zM^J2-wHT=Owa>jT?9JCqR7DTQNmf{!&c{q&-u25}R;)f#)%`m+5L#6#A$kMPA4vog zWvh3lHdu8p%$IVxKZo=ZT+9{&o`#<=GsJi0GqA?`hZEJhHr^~X^Wm@DeW+jP=-xW9 z{cHi999fbRGhY9_?cy|Is>iYQmQNE7wk^Mlo{hY8t5q+~O*@apn_>+YE%JK~aksEA zr^ZDV)FF&`-&^6SduI$O=q?-6_Q878?Fzg16go!nd9dch!Jd^5>-jgs?GP`rp3oqE z6++e<@R3b4zslplDuY{#03}EX_vXg39QcevwUwnE-;5Lxjmd5flL3ldD$nN-;{q7MKz4kPh}a?KXhf;o zIh`cb7tw649VxU~o^W;5l)p8Z{oS&=P``qB`3?)joTSx@Z;|zIA>13WquvW2ffn(~ zPs0zF(K*RBXEf?DM{rIGt`LANn)dk6b{3oQZ%$la?_K^A4Y|j{f5-;8fa3pu&_BqzrX)AAK`CA|b1z9jyPnkaJ$dp%0L%#s9?n?ybvZPG8heC}( zigW5-TZ{}*2xIeLhJrX(H^wtAsGad$=KRPCeCpUMhudczv+T@V?J98O5`UJocqY( zBGV#onW5{US{-+|zuk?czf5b_Dy7rrR{i{=O0E&^ERz<6+-T2b-1O^!rFS@$D6=B? zhJ@Yg1G6p)f~=B5niYR@_inU~KM8NFP)byKxlT^Dt6a=5m~=EB$~uEm85|PO`D#;* z&n18JS77;PL`P-R0t+Fvy-Cd^nc7%uOHxg)cScg_!${Y5uPc#fCGb;dCJgl2iG`m^ zmfRad{($SvogalN8e@BaBJ=rNUjMzt^0^-1K5uAt9EHSS@iDrnISW<)0e>JjRLDG~ zb%a7Dps3kZ@uQ2a(cue3ppC*Bj4=w zbHb#ruj7C0JE%HDVT@@YX50`MoG$Wu#EdG_jzA~OvT-FzZ&OSs$c}`PO@o7-+t<|L z@o>M2cYz(PlQ3JY0g~x%@q_|KmDu~Y#I7oKw+h=V719lb*{Q<1my%7w_#8*kFeD7n zN!_Z$YGfYxm4Fzc%E<9o78P2_uZMKI8XF0eH@T-)VD2KFrMeBJ+eQ0;u z(M8nXsPiJvZcP%KZL0aD8?hw^CVC5p?$G9Kk_)MgJuoArldPP7F0yLhUlF_Z>~HKL zV2>US;?sWSx~~H==^`jBmJgT5sTMUDqW4@ptl2}QWIL+!Ot9aD)KVO zA}HhUpR~pDAN3@?6VKu4ATZSV%KJ@A{t8^lZ}1!vrP4e``Pt7+t=o=LyhkICK8D$|{p(O2v~j{G`8Rm|hj-yBB@v9wlVaC3npLa@@+@yFSf~3l z$Mj!8&EdK(?yrmp&aVqlW+|l!e|;g#BFOFv>MJML0}vAaIW4~rP* z7p^HfWZAo$Z$N(Eb|OQD=}dYF#;BS~yR)W(RVDQP(X`{8U9~9RK&Ym0=m{(`tCRYhQYM#;`*G(5ZM@cpkd3LA$LOW8s0TzunwvY+j5;D$ zeBMTINdt(EiMWp>Bzm{Tbmz&Ae|H4o1pFt&mbMcG{|H@{OI1Hn(`36~o`6g_RW4WW zbCWSGYk&1&xK#E*pGoXnoD9kdeR-AgRWEdkt`oGe+ zSt`%)L|rQmUNZ(u?}VvmV)dQlX<^2~O9|DrZLwm{@d@pBlP@2%VSFb?cfoj%J(xPc z>aFQ0#Qa{pf^~`=*^;osx1%B1!))F z)IB+xr5WvL69($O5s4YXp&cT+w~oSn?H|@mzkRrU?GAw6=;sCqXaFmRuYs?uh{vBx zV9=XQ0E}FN3`QOWA){|7d@As01v*WU{>KFUKVTYOGtUGEk3us9`8nnKB4&ddO8Z znHB_I)d+na%(Ln;Dn2057toT`5tKF}Vsm;sqdM_X9B-G1_HucRX5u57=e;Pkb!QgI z(K7aRV6_TTg$6{I)ef%!32$DsYSfMIeBY~Mr9=PZc4ow*V)QFH%GJaBG=@cpXqkXoH}x`t)Bx_I^5pJ zY!`)y-!Cp)OwBedKtHMok#0Cy3ojH}2ZCGgUM-+aD4@Y89d0Mqj~ZI8@47ttzj8{5 zFQ=+p4(Mn7HZbln@)aN9fbVYK%e#OFx3_WO<3B>jTQlJev-cs6rg*_EFp>!ZG>d?_ zwBN!Ez;yPaZ96f<{J`Q0^JX(w7p^|33VmP%AhxAt>4dua7=0> zq$=7$QbL}oi83-D!;LRZ>U4ZFCCMe+;eeVbP)iAvpg~qa+3SFs7(j#vu%SVAK-oTk zjS!%P2lOCG4uP`$07h!S02i1-k~{{++<+o#ARG;nj3jyfUnc&OvGC`F{H(Ge>gk)v z<1s9CuNHYoV|t>=pMt$Blhl=W{7RTCX7z?8L;u8B$A5VISd+Z{*I6Ja_68_g4gsa3 zBo&-Ca%W`D8DvC2?)i3pUOr#@B5nqpT&Wl znJ18ERp`M;o$8+48QL^%K^VZcH(2t#8HH0m?*NqUZ*;~1qJ^mdMJ8xKK{d-KwhWX6 z3XpP_qSN{2W;gWuRAgi{%FUg4?Q!NXmEN;hBHf$hfD|ZbW&0LPdk#|TUC|TT8fIVH0N3q1%S+&@zvCs}pUOpq@`0YAIXQ{~82R#rOfON+m#U&dimMq5;(Sy>}vy%Cl9ht8idV=gyW^Q z_0WH}3GS@6Upt1o-0n4k?Pdo%D5_fI2wVwwpne41;d=sa@SQhTu1QT#WO5JV)+ zfXl%rRyS;`C?wXGD!|hiA6q z+gYq{%TGP+rOffirH?BgWSbryKJ}-!O~L?NRF%&4I)}Fch}pP;!%Ux*s5r==Fe$waLl7(&}x^{WXR z*sey5lMFDakn00H0#}#sqr4RC>~GxcrfEI<6sH~uWSSTW2a^F_^E;zi61#&w2~lu| z!DBDj#eMyfQ#9^g>4*U)V*7|X5kV-uHDvQ8YZ*U!#K;GP9W3(Rll4TGnR)%L5@6Pr zrp?4zm(2N#tFw0GlAlag+TiQw!OPg>=#U*xjFtWmHD?dvkn6zApL%t9PyxW(8DCrnR237kzBaQ zp1LAY`2Mu{GJ8n!(l^I7d7?@ zTh9P1=3)DjNGtV4KZTAI;K=l14aV7FTsgCF^V*pICg?(-78{r%1Y$q=VE{8>ruRtK z2g5ZTIhKDI;_?9y)M9#=){BHYP-z`3weW{w4xa#fa8C(uc>O)nNC)&{E^7?YbO@&L z#{qOjE?Z`9{cdN|DX%tf?%X6KGGQp?e`uz42B_+|Szx)jtDt5yl(>aCqyM2J6XedI zU}HKLZED7aB<@cbe=njUF5Xshx-5CoKP0yVC+*ax>MR>BOf#9m69g^)2E$(;fLj!*d?D9O=8Qg!PO>23))%4W&_(dk%;@uASQygZ++G2wx&q#p z-{xTFNvA6}a;?GM2@d%{Ye!2^qu5zPu;@Qwp=NJi{M>9P6}WIOS`_)myDR|D0J1ND z>=VE&^RGc9h%fFRMDTzOp+9Ryfz(Zek`_p>d}@cA*N(46vy|_f!Rhhv8clPHPSmFG zXw+3`1k5|XIg^rg?M(WLity1YRhoMimiB78^3GF?X*iX84P1B4{>0i1b3QT+T0A z7X$K!eat{i-ykoiyFai(CC@LMu7ceW^wXfzn9v6g$l4{(rNja9|j`Nmx&?! zvgJ@qIQg!-ANWr-BV7FC>6t7?Lz6_(Dx4!YD=C}Ht2yl!85W0cMh^I68@5?!dx9cM z9Y?k?{+6OAaPdA9`W1!Kp(b)NIKG;xJ>SbtncCC9FfYBX%Cp#6rxI`w7uTNHoJ0mr zDG?PX48WD5{J5t~4_`%JBTglV=4mqmUvZ%P5mN(N)PEZTQ1iV3D6zxhu=9S79bkv$ znF$8@!s~vzR5h82$%)3qi=ZWJzdE%!jrhVoaDx+k`xb`wXA~6Tb<&M1qh`I|e~JE~ z;A=Xg-(?-j6Vc)KHq@g^BB#@x;w!|b`fg4~8&Ak!=Y$tDz$C(JrZ|w7%Hb80Y-QH{ z?yF5*0GyAc^KHp7KSaF7P7+5Pcg0k~9R7D6Y$I{%#W~;W8??MJbD9Yr0F5T+0w-&q)7f@@qS%2CK8si!Po4m=cifafD zZhTSoAP;s0CmObtI9S1YchH-_M#>)Ne2F#SuSX_i7wK1@k8K~R%&kwF3Ff05Ryz_9 z_UdB>@~BV(`)QyMe&bgF>Gj4GFJ$CN+QOP@HGGiaG2VI#!UpZXY{YJ-MF@@itZ89q zA98F=(CWp(zZiW`8GmELM|B%S-uc=XXYMO25OQp#>BgY|6)#Bg9qAAZ2mOL2`8Imu z0|$?R_ZMlhz-BG_nS1*d?0e;O_DDkVJC^ZHKycd zeEGsKS(CK5xm6P(!2}u};a{+20hxk_FG4=h^^wdy075@C^*BlLuKH4rG687M=?#9J zQhfxz*yAE{*8P)8oBIS42IvPY)!z~05yRn=}EBr#7PN)e+7KZU>)T=&$aPXRtx6Y$p~Ni7g&&%{F-+NHP$mj2oO&B?2HG_N~ZT$3-S**nX;yQ`j;M(0G0O}Z-!!t zSxO^+_b+GsT+AK|E<~2uHy*Uo$sn=+a~hHEuw9)p*_{-YyIbuiSwl2Dg1?9<0`#lz zw!fmd$mix(SyoXZ(+5*X-rH_1fL-{>M>h7BZ30!?5J8VL6GYk8-y;VJ#=Nmnj@Sbzw%jKdWO^Nt!`PbA} zq<_F7?0hmlNM!_skPXR4dA$-q47%fp$~_l+@8y!^Kd_a-l6TfM&K`6Lr}&eWNe~F37~BL@`2=Y&4{9;? zJohnRp5yZ-&l)oLi23FHWrUjy(^L+;)%!2ua-ad!DcAIEgCFA#yAii)T}F7D&uCnW z2Dq>LGJGl06IROfrY4>q6>XAp1~^9%mA{R4G`r>fRf4@s8Q@@NfBDO#l9}X3)6`LE zD9{x3aNF% z;wj&dTa+p_5I0@@vvl?}ym-C0j1LlN^u+3Ts-)`tWzK3K%D>4yhNqe~lfIvHv$HIv za6YS88F=LWki?=dGZVp!mA?rEseepcy7=W>Y5x}^-XV7u^Tj||dX|7* zD*&-EMZmHSNLJ{%B!{l%NZ15}Iz0a;mKia^p8nUfn_ z6b*#aL*fV|J^%kRf%x!8?l3BEA#cuiX1`)%=~lMmI_a`KOveZq{cdm=muG12HSO7Q z9Cg__erUDVc9`>!q*Jpd>9T*^(b8+)vt`Ks@O9qudH&xus^62B=;a#n=M-l4*~t+S zQ}>^L*Njh=otp?X*IG>+Ebv_}Vaa9P_WDCZSU4a_bMyWiTVMD~o1JwzS{b6nBj*1K z5)5mm{WpC~?Y%^{+o5|0bN*{v7XbpNCYLLNSSv-`hV6bFRENICpR@xTN=bhEmDz9G z|M`%#b70b8U(B=B+L1rw4C*xfy-hco{nTpXuW&r%V|i;sbg-c#-sxCyOQF?Tn|rJC zgs3lvKb|7?N>MH(+zVD3hS8E|H;*X@3^S% zp|5E;&Weo&l>F7APD0GGlg={RL7R1qz-2^>-$vU;8cSgYd z@Yq_Qqsi&hn^Rpcpg~1tbfm2>3wk}bJutd>u<90Yj%H*W#lg`k&}tfrh7U9oNCLM1 zsgF(w?mc|y_-lhITtxiho=f-w#=Vp|%EEyO!rr_zjj0!>Ie6gXbGC3~dbB<9HHb#| zilkM{B{pUJbOAR;9DYs{dNl{!&zZC_>W4ovrwmr4c@{0u+8*5PrcM`EvcHrWZNLEU zNEKvzqhLq0e6*~pA8<2#VC{IX0@P0j-P?@^2ZYqDNE~yu??wT4B#!`7E0=)=zNqc8 zeRN6nSfJaP9$45`4UuXX0tX6O6`=yAih5eH}3JcBiR_iVAGA9qQi-2nf za{@?ijyu=VJ0Kusc>WYUW>@%zN5a4wmtnl@e*R+EqU!Cgukz)Dud$cJQsw85-#j*z zf{n=EUoWZ5c@-BKwp?r9>Kz2=G=*%EOpDKDA)AJU(brFS+#bZQ{g| z93N{UJ$w$dM_^?HY^ki(dY$^-Eq*k*-4;E&?(EqRW_8!An{P8*{N<8-mF|A~k)i8n z30oRR+o&AJ22@>j!BVDceo0I)?d4|8pf>NaWc@;-SPI`^g$$ug->OCbC3X)i>Y!S^ zM7KR)ZXCu!AxyFsh~DX~e`rB-HhmMaiJPnR;p==1g~xP#Pj*dBi&IQb7 zD-Usk`Nmd-l)y+#E{^)4@aEf6Q}TkI5#6M+dr!7$l=TGP;mJCi+$}*kX^%fH7nZTX z$3EHZ>ecm#P$nSaU31obODFt20>L@z>#7*0m62kRXgO>-LX~DxR;Hk6k=Wi=H44p@ z*1OL?UYaeaZAPF3)y&fT zuAKL+z$0MUvNhz_8eW-TYl7G}<$h)^m+u>EgN?UsWr7R7Jho7Jq?2oI|B74~*-%DC zL`DZj=a;U&gj}ngn;G!Y@4N?PP0{|{oT*jA?G~IbLBebGsJV#K^@s`8%6{#9`sLrddH|nqBpyFfoE0?WM?t_Kz#bTb(WoF=b&u6i!$0(WcnM!Fs`C+lYHGsj0QK~~*C9W7IGuIB z+v$v$9oBmc-dEH36f&gPrD;MVk^^XI64(C0%MGWS%+^NV=4{h7HEN*;8vDWsZ;R{? zJujn5g}q}k4j+GzS+BEcIC-Vd<7JE?IqrJT_2p93S$TnkEi+~yB}EK3!CB;9oWkzY z0ImeA@AWdGBI9g3(#bkVQ^!9)3)&BhaoZ36$PxYyy+7@N<79ZfX?e?`s9myg@XRrZ z>Mxp{`IqI1SK)Kc`8kK=-S8)1{9yLCQ&@Ij6vV4&pj(Lma^;RpYZ4B0XW2LGKjeE2 z2BAx`f$<>*$h1Hmx+Gvn00AZ+0iYro0IU`PAZHW*mx9`^gqg5)^h0k#)!+XcqadRq KT_I)m=Dz_d?L3?S diff --git a/Apps/MidiController/images/10_chord_mode.png b/Apps/MidiController/images/10_chord_mode.png deleted file mode 100644 index f33dc1d32fb969c2e2ad360b182dc14bda43aeeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36999 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 AssI20 diff --git a/Apps/MidiController/images/11_lfo_mode.png b/Apps/MidiController/images/11_lfo_mode.png deleted file mode 100644 index 4cf20731fcd113c4e8c9c1c32eb16728201ba9b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38735 zcmd42bx<7r*C$GpClDYB7CeFA7Th5rSa5d>?(Q}*+=2&pcXtTEgA6(_$Phd*I1J9r z?IF+a-Mv+}wszmDt-ARq4RjwrpL5Q)PlqZiO5h?pQe6WChR))H(dG_<*difK1hjR8; z)_e^ie96{heO$he>{7Ttw82>7vQzA!Y}hdShOVr1`@ZV39mPpw+c|w*yTbP^FPKxG ze5RF=sea|~aHuf8=;cmaM#QRfL=5Ydu;N9)#&9yqd-V7^bLm)~orx{^As`gSk4(WY zJLn4xj6Yp3g#KRnydJ>4z4}P`xaQvNRTk#|@oip%B)qL7dsVjXb@Zg+eSLLrgNEv@ zd4pM#WjyxblyJ6N!v5do=~BBb>UDed{Q7)MLM6o8G&B+@cArJ$qACpacFT`(0~*?A zAx-rh$9u^vp3MgE&Q?Q}_z$a}Ha>O^vR?({UduL1Uzi+h*~?r)!YQh*84PL&S)ax3~F4kTh?^`H&i%BUvF*kIdZYDVmsm z0gI-bx4E~sMsp5BQX0z!zZSowN^=2cf^48iu(*A;88ze8lT5BCe%)=Ba!`(;Q1(E1 z`m5st5;j0QGD>LHvTx>=C7*s-*)2wdSZ;C&ePQjAZ(w#$)Ku4K(rWF0s&OBC7dLQD zd{DwA-n#Fpv@HZ;mFgk^9bEHFL6zMU(!p-NM{+ zyGIqiPU*h&dJ+7If)d>bP&3j*L7q-+Jw;X}PqkQvpEGJW=ZkBg8Uc^3t<;t=xifc+ z3?POn9a)l~+%B%~X2xhX{h(R%0C9L<56w3FDEXYiLI*Dnn)7RD$&4m*7G*B)E%GLxaAG3r*OVfTDAIEG$R?hpmx0Rnw%|inDC@4^s&f z78bT}eTLm>J=9+5W>Rh1wlp}BjBd^FD_}YM&mlrCl^(W;QxHs4#r{^pq^arg`s3tu z-NmZnu62*d%4ltdMB92RhPiFOCxDoCJ>S;b(Qf7+^h90@m6RyTNTe<{AY0+}ExWx9 z+S~=l6lg*}%-Lq2EnEyMmOIQ&{dQ^Xn|sUh6uVwm->ZGZQ z8}ln_1d>zB?aO`TkuHvyXoc96x)QUzeBo^?fFK`^3q}km4CdIwr&Tvt@Eu#b*+g?i z#?7dq6njLD_MeA#pUKq$q!0Km%riH*q@Z_1EoS?VGkoh{A3C_kx?x^2izY?Mt@eq zX!GQ-VE$+uRLR&Ku6Dmk0P)LhVKg6;-~T1;vG~^%juSHs7u8zlSHPyupH9AK2p_ z;ojV9b9nQk5Fo8qtPW_Ld;eBnnKj3w@Ww(2*RQlUu5VI#=}Jlf4A%s#rb2LL{;7rK)3{eu?wyEc-p z)63UJga(Pjjam0-b*!nr+EvL?QX|l}-j{p`5WvclVTwqWE;d)5%fi(we44WkFs-Ss zAwW%=8N|K99S7z9iwF${rs&pwW) zyPJZcESmcdf@Q}e%#?fYqH#EyV%{dzpZ;}^=>03F>VE_zn6N6B1+I`>@+JJaS;ZH7>c4Mx(hl2euQ^^(n+y+=GqXgi^O5}% zd7O9XRpLrQBa4nZTziIc?)ca8s1}}kTZ$&8S?1~yPkT|5gvV_Q@_k}B@v|!UMoViU z!B(>_<~#;=hDRytzb(Jgc*sSsl(_|`5(JGdKwURBbviY_@i{7vPpo{F44gY}{xtRJ zl$iQc=HpkZHDEV^6hn1=YN6YXMP5aKuGY7Eb%<#nBDyN=1G+h~-KP@vE2tDZrgPmG zWz*HYMmb*jbmfTKuPruYde~j{*9pG3@=HqElI*3exy9Qb>Tdoc{(L?trkS!3AEOzV z7U=XSxdUlOI+`qWl_=z>x2OAZ+x1>CpC|H9ImFUD^GJ=Z*Uq5H3HJHI^_ciOVXv^> zxaHC(FAQ;Pwy8Ud{mr6MKRIBeq_h4&HsHGG=iPZ7vcjtPd`mu zw7k5eFS$r-u4RU3Ch%82MT{Rf8qO`e{uUu8Q1G6ET~~F7L5G(e^eG;rJ54Yk#}d^l zjZ2}ab}w!mgr>a1g|50k2soD-PUcY?U>@d9)FKo$hq(UKh%4X6rey>|WR|yXmTC

o4D0%z2S4(NfvOUz{B&p+P)A7_yJK;w14FZ0bjZM zcArT)pSQmQBjHM-f@kAvyF6T;8~z|y(4mH$#LdJ;r_dPkGcM-v;Gi?iZkTp$m~EIk0{UfDb}o_%VK)9As%JGUENwTVzml7q zzt9v$QS9IR9;k+$Uqp*RHrt4~x~#CY~?6-)4#JG9UZMx zK0`Aq_*}}o*Roy>deVC=q$dP|70-8!-;7VvUnUC35iPCB()uV#;VDi?ne#1HU8Z_I zkCfUyo~mATL|N=--`mGIIWsz6!(SZr<)Ho|^Gxh$x!9DxL(mq&GD&EnoaY|T((1G< zeq`l%sy93^G?e5ok7oMKuETz7Q4hotw&~g6(FrR*-W$vv{L3@3$x)qbsmZs=8{ahL`@XFQ)1k24W<`QwzB zZn?I;J|udZ&;w=-1XDX!yrXFsbL@hEc8s*|j;uKOJU38!H3I>9-r%9^I>FbUMwm-F z+h-djN|h*tkbTUZ>Q`#2QOV7<_qKzpyMYW!H*XR58yzw-*3o|MfA)q~5KOJE@Yb+%O~xZV#!R`>$f(x?97)Ila$1MrHrQiVg$i+W^l#|-UoS}F8M2E zqR!hEUhfB;Emds?d505alF3laqdK8{EUjBXwn<6;kT#)b{=S_H_oBKFy{OX_*{gIV zMA$)Hf!M^c(!TvOHd2O$9XqSnj8#zkMwqkhWv-i%(Na>W0Km*iV2C5RG-J*oO@QlQ zbu+g8J=D1%9d&*k+7k+QXoIP`EY6GBi)1QVPL9-6IU)G=$8I$O`0X4=s)3fa7IbRk z+XQmh&gXufJEBwH`Gn(_Xb2^X=Fl$<=3ty4k6?9;5i@VZ-1Y~W#j6(6@!8*&e@7E5 zL<5xrP-**8#@0P_u*-B8n+v&;WfYB%v(JDUTq|H_QL8g$l4PaC{ZuKW_VMD=Tn6_< zbOS)tNc>2e6NL?63*$ih@}Yihjo9@lkojg%n^D5%B^QIMA(@>oriHazG_fJVw4=5X z4N09#ulGa+1OytFbc&~e)B$aRP8Ckpw`A8)+hsb(6~n~!{90LCViQ{@+NsaYGsW?M z`3@Llc#Ck0vKz)|XA%YFI-5M^@fv$`s=L5PB}ZK?KKAZ(_QA!?jeXTq+0)}0R-7?u zKQ`uYLRc3{7vEv_MtuP~us2n-MJ3zZ5hcSq8^s8#`ldP={@4p6!2I$&7IIxRzgi{7 zVKqWm4yX&S-E#BOj!(6=$N_%4KmA(jWEE=!A8FR zE0s!~D+Hw_zg%5hdYXH-V6&3~5L_v!^fwD&v_Z>UsG+W*@1n>VbABn}!V&@483q4p6ntL`0bDDrh=Hak^Uft@PG$I>R-c zfbQRGZSI?+iwX6in(Tdrx3zXG7eu|>i_(h3;0K%PZ(r_X+ztAWw=`8Zn5SB5VP7_@ zE^J#oW8}Z+-1qj50i&Wr<|86hmzr3SV^K71oVm^~7LKG>ji@{ZR%V{0)Ic$v09JH9 zA5PfvRQR&8PodXEZi!y5!G@UXM_oa2pj~#j=x3a z&~jLOfokcTq1B`nPlKVJo?I#cCQ(jZS>2V4=gIv87VD%9`F=Vn8Rh%S(B3GzUxta&cSip3yQge^uI?qx^!)-B}XBtLh%;5EIwipbE{)nw|6;=+A2Cr2#^OQE+F*V0YZ^Azj2T2C_c z5YQI}`-;E<^g7oCu*<^*ScYVeU!pa^Skq9xd||Kz+H7y`m_;lY`MG!*4OOtlJd^r@ z?>2I)mY#l5XzROdzHG37j*j+=`+OA!RAeN{v^tfo`=4q8x3;R)3%F$qv^EBV>2jKX z(nZEl6XHw@>)`v$&n{@2uss@5Dc0Bxp3_Mr)EZv5c!V2{ceHn>@ri9+iiw(Yp8WP$ z-QcU96D;s?3@1rYw%Ky@${!r%yY{ll9K6DXE7{u_CDg>our{z40D)AbaO*x(?hx*jLfb z9>9D51XbuqW+W%4)Ea2JU&RFlmdITBeuQhw7Au&kM{ zSx;qyHxv`m4tCJCf3h-BqngysV@fK5<=<y`{R9hQQB6c;Op0Cu-8DQ&DL8jt3m2Huq*h1lw5qiiP zq+zkSj&)f7(kOnoF3`H*im%`EhLsASwkn=o<2p`;{Ji?yxwDnFZD zb#-%fkG}XP*>W4PsnHrcZE;)IQtn{${n~J6YaPb!*IWe}>Y5?d$7NhG6z)D0k@{Ed zy4^Nknq=seo7-!E^9mW9S2-s0n?~|wEdj9YH{}9}b z)OHvTYNp_l74`9x<0-`Z9;8$@>!kBod}E`?5* zkXO>3K^0VZ`cbQk1r>3aExv(G*(Gh}Gl^RQS{0Eu;X}+l54LymhckO-3dgd?>?5Xz zlCZlXZD;mj&G@vg3Wq^J${g}Yu}{E{&BR^D-98-miN~OW=*?n=VASeQ2w5r_GZQnA z=d~&23*}GecVwAR zol^KgRWz?Q6Ht_H4jn^W23>9q{>_`v(Jj0k|I_XCY4JAqdB|3)mR;#T3I)u$W-GHH zAHD;pINZ9CO)LDXS)VBzr5(q}_(%a5yp+5A$W$*IVO`mS4G~sRYi{SX!z%19o`e^n zOw_6mvT;gr66wO%1?2Js0X&W0B0G9iIr(x-_Rqx_TY9C(k+9Msys|?PL~wmV1!AHb zs{|5E%39;RhP>Z<(QxiF-qD98Hl5m6UJ+B-h^B;w0%t3=+(DU6!4I~APk!zAm2>Ug z=FAI0&GPJ06u!{{VM>eQM16LNep{HXhy7D6T(>i<&^(+1!{|8DV(q5TBr)Yt2jcA%Mw z=`AohZ2DP{<(dnYegsCHdmjqhW)y7!!mFABXuPh24L){*yEGN4OPWif6U5`ElK3?P zd4FgpweI69?a7NEKYLbtSTd54&N=aZbPVp>oo}O`kbnBeW)~RBl<95`+G0?Ap`0}6 zEEx{du-y_A=Iww?z)YDYAQJR;&wi5M?*63`dv_YnmzSw*Oh9F+xb zUW^giM)RCV=kWc(x0^jmqn*%_+hcL2TJ5@rS?DQ)A`)-Jz0&Cu{hzh*;p!4pFrdeC zI~zR@foxfSMg-ypI+*6^|HJ|g=fV>co81h5#;U#&U2$7!t5(iyDgP)o2ASYp&CLyt zKApd$6-WbfJf^u1xTi^5MdxXXDH~sSbvu~y%#f-6SJm7LSO;<&&Q&0(8x4}8#rGLH z_WS$*@$jZ>(W7IIM9?cwb??qu#Ld?X*FPT4S*(Aw=(|{430vIrmNfpf=;lT_-U8G& zRkfMHdyDNK!t2E0E{#MAK!<1ajapST&*xr(uDNy1@OhDsrF&?zM8)8)zXu1zibt?r zF@z+D11K5N5K7wxJ>}vty~=qZQ{5j_GJ95j-|*V=mA)CjO@GS=ClCf-1^I~&*OYl> zd=Pd3N5DsGN5No)!B-RZd91Oba@74Y&eg?Umqvda>3#)=|23dYSus4Xn~MRaD$ne~ z_kc@A&e}aecls6qWFGxj-Q1$bJHH1sF+hkr>FH{2BfJ!<1&fJ8u24#Z`b6tj) zbN0LJ`R5NDUBrK|e8|m@jHHJds)qGQ-)4k0?O?dw1u3luv_59+SJw5*44JN+-QO;l@AQFtXwc;?2(EaUp)vW5m{2+q@IiV{9h!^vMgM zvEo2W)VRT4QJ$}`*Nry(x$Nq-XiJHfRYg-}k3k+#w1Y}qUW$$_>Kza8)x?wJc7={M ziamh@+5$P$)R=|a>tM?5IZ0h~>kly>-+*@$ZtI_o>|&+{kjt zx@p9}C8Uzk+Pj#T9IP^jroC)el-wAU_=~pcbvde~KQ&K9YiHJ-LL2XZvK*8s`khs& zCb$iwXGDGznE(Y3B)&s-6v#`?m!clwDc0=UmnH*$v@JTp+DBU6H(~bSIh`t}h`Ekt zTm2dIU8W#d&~mq7yPAoEDEb6jrcX!MA^LfEXWi?QNG2v`H=p!{&D-#*TnO2uHaONI z?m6=TI&q+}5ckV!Z3JRV`);|Lwh}gnd4+Bw$&!?Kuy`ZK5gsnq+tZr12iDdqoa`;4 zZ6Z8`My4m!?V?I_+{6mRypAjc#eWKpr$@);|N7#Cz<;z^2`itW8~G4eeFhDDlRN#L zH~A+K{+hFI&urD4^09=1w)nKE#!A89w13{b$Zy=m#R)S;UP7+N_$gA%KskQr%kOi} z06tbJ$jazG&7lSYuUBtV$I``e`AgK7q6MA%VJUMTyt%@&$eCDh!^>~yZW4T($ zd1lA!mB_7jwA0?yJQe_tY1FE=a@!8-FWE!u^MOVo1gI*!?&)b6wKh5%_j(0icI=!*Ham3Mfhgo)dn{k21qta(&8_ZK==z4q1pFlBC=Nb1`ry1Mf_Fp89kXtqH3 zrY7C@xqJ<9XCka`l{Wx-IlxB{TU%S}xbrEbQDc93{ZeVV^Vc4Csx3BPJ}=|_rx8#H zz*1oLb>nyUNpD_|2Njb=M%CRY3k}u$EVqseMKU<0IeRTo)=QPEmiEBzpA;R}k5&oZ zG$1ofMH1$j1N8*iQa+24`0J#w|iq6dCxgFMHt^eGFgm=eo1qrR!j zk#Qor91r7ISL2!ro!MRY4uBMX(9=s{QD5+eyq~-gHL9GyS#wyv_w>@{MCkn<$L&f* zk(@tDU;9)UxqWw&aWl=Z%zKSY-03!2)=s`^3~@&9R(d0k5f_d6=|jlV?D zoe0bjFK&)Hjxy)IX|>7U-*(I~i^7T4cM50%#yQ1*$2luD=Lv!K?}<4D*i^;1%~!oiI{ceJy0JaCyC1^e zzf~LC1KQ0d-#p5mu6f-#3l_wY96NJ@>|s2^KO)s*Saqfoodw5bY70>o#g0XVA6J_w zqW3~_b*XCs({fnoG@Sa@6Oao#DWBI%gGd|dzrRNJ4|Ippx4rnJK|`mW_UTw7Wyg~D zFM&d0RPVZfmiElWDJIaH6p8^e`>fHWr~xA-j-uVt%yP?Gc;&log+Hzp?1t@b-pBXM z3C!4shK2`8$VfsHcMe@vSAn67U0x}({%E(DvER3_FGzYNm4VTUIz=wcO>v*eY)7x> zxUCIm*2g}(5wZ$dQ=EZJ^f@m?C@tRGJ4G#Iw2Ze=Is&sxKZ*1Sw@u|LJw>7=t4hZM zg{SRNjswyH#pfvyO87~+PU7^I`ypY3FOeV^2$&Dyf?wDY3ppGV{^+@NK$1^PZ%1_H zB)fgZhXNzAyAx%!u0GB)*r+;_O(f7p#ykTc+Ku9W=q0ip=97zC3~ufia(#Q&d@NqZ ztSe-ToIQ{+igpJ;ZhVsA(*%QeMp;j&l~f8^mo@tv?3^VC*f|Z2u=I@`GpqH&wp-`# zgMELnvQ0z*O`=u$=0&`sG=deZ(}AMuaD>+fMll}XTogqDT{+NUTJGkR=SLP~Y5jy0 z?~Hd#TPJ-K`69%GYc*b|ZM&Wr!o(wg?5|R;*?;Z^DW|X`U%DCEpJG@7YC3(`Gv=yWIzgJsp5m9tP7nEIxxUX6fnXNyopjelJ!kkga2tQ$=FRC)SxI1H02<^E_yr#O)5~ zBht+JO}?fIo*Tla&H3z2CAPa)Mv@m%wl|iQ+MEa@=Cd=F6Cwke`lmO=ZT+_qCQ&>w zb$ty|IaqN8M@PfttLEr~&&(OQ7@q%^n=0gf5-HVE=-U$B$Jk+O8MhlE7;h-?P*v^*>$#ic zR20Z7kbKpY9&0NV=eG10{P{EYCbo-bjP=Kk$eMvVq#u|acG23`6rNXDs3mr|=u5{{=RgLq!oA2>fXNM3Ep=Wxl=f2?? z=3_}X!>BP4`>%nfz2ibawJIbmvSeNNWx&;@BaECZI(Ojn@UU?6-xtFe-})Dei-puO zD=sZU1KN>p`&)oT`V4HYA$joF=!(!2S8$GRhS%O;;1>3h&`B)Vo33lii|rh0nQ5)b zQuuvDUyz5#-PMidLE(27qe)_-3Im!R%OzC8*>&&vHp{^9aGdXLCy9Xp)fYPJtnX8R zGHaDHXfOLMy>#SSx>4fsPBH%yh38|mkQx8^Q`gJea~RdMQcc!d6^4XX^~*awO_%3* z#CJqJk#3nHd#c!+T*qsa|Gt)1{1RnUxp{4It-Veb!BUF0W%$JFT zGm3sMtI=XN&}5xqN~$8LO_t+wbpe_q(W}M%HBv4Q?1>591enqYc4LRq!zJ%h`s~li z$=)t_w+jOpFA5n&y%1VAMkvo|BW8U8USZPbk7bs=IM*J=l#~;+9j=WYF6qB_a0Xvs zKQ0{}9m?nbqSKaB^>Onv{#(OK4%4Jk70~J@&h=-SP41aOs8KzUM-csQ$;tF*`hOkH znfkmP=v=Wz@XEOeBXOc1KD?*8=dfUReLP1&a9x84?i9E&WNK*g**db!^e@n-ZFCg%jk&g@Ui^E zW{o8l9ZM$Nx<8a)foSe4hsA0HT9cETq)o9)NzZLwtjD+)q9pn{+TRirr5Z_wSAeq- zPFEL(j3#$@?b3~6iZ_kUx5NU&YCna;&T&wChz~xISbCZt5Me%GBl%xMx2Z#M`^FOE zcev>;WZ$p#aG#;c?{Gq_?Wk6XUQA)p@QkHpv$6x_)s-S127+F8W7EUpjene+xB*A% z5J&Ey@}(vZe>X3$lgokKd9`Z=#m|cW)P6N+dM(1tPS4E8=c{8lI~hGHNKvS$q@X}+ z+j?>FKyu>o?VEdygZyu~dN-D+de@lPo89tI)Ws#L>0mifeWwOU9Z=^L7-i<%XZG#t z0#rtFh$|yN)6Jxiy3a*W9_KPS+YCuPKUnfP z4;YaxuX5?wByRX3ma_821$;KmiAL)^%D-ml7>j38a7L`!HU?P6#1d-yX$6Z?HVJ!} zE}%jk{mktAPAsgD0Pf1Ua!xaICqw0cp4m#%CJ-_B!gIMNwIWvx`=Or;_(X&I|3&rw z1c`a}Mc3yEOza`b6nJaWCGw)?W>K!ev+z7?!PU?GR+ZMjY5$R(q}*98DnOwk0H+jE zWy5+CD7t?A4s?EuY9#~|*hFxEOrf&BAD>sIWK;Xjdnho%?%-ffq=oJU_ zKUL*{#lKe=9|;It`2M|q@!-FI`v)X7o!oy6hM}tXf0fz)T~+?2ZuAnBt%@>3g+PMGrN} zZ{{gEdA_f%c;?nqSsHZwFUpARvZk4N?wGR_DCX5GVNewvOs% z_14O&^eENF+^J#)B4}lm)M5)y>a~sFg@(Sz6Ev+KGrUPQXRO}fjS*PQQS7tQodfuf zk<{ebsoCB6M)TV`{xNb?)r8ex;-O@<*U&XBttNDE1~a_Zq}JiRrG>*%2dfb9z8wR@ z&gxfe3!fuw$JvpUvHDk3dq#+JXm;rMAnt9KLzYsL=isR0{XSH~V*?@mSRq2u1HCbh zF*_E2tw)i+hi3i*p71>VdDadA`VUpkj2EYjMAF4-9&-qV$I1~Z|2~ivFMLIw&n8(t zpxD|GdMAwImm+$i#2|~0wOk@3V%j3hTe`?*s~r z=AA=c!D&vF1({Q+SpSQ&O`;Sl@L{Qi^S&G3lwgCB<*Gqwl;2*vWo(7lrj^fYX;381 z3x_n{w#_Rxp1gtDk<&$ry`Y#A<5y5y7Dda#o?QNyp1f$ud*7nTleeS1^4oO@_HjE2 z-=D58W$Ibw+>A<&tcW)4_EPN8Z`WO3z(aS;wAyIMae_><@5YizAR*a_|nZ^(%u=Q?e5_NwGgr+uZlm>o^;mZMxRDwIFd9 z!81}$b-)+CuXe8=Ye3Dq61HsfN70S#(smWnZ4uPQ9ROiyfR@;OT%gndwlPAu`Li)E_=yTmSjz5An(& z7`5}LvfnY_vE(VaX0K;Q?e0KKca2)vJ;H_(!E#s;KuVOXQ_s>W$G-x$YO6cJYH+BS zYqijFKqXn*bas((Rlqv_l4A|!=*Wc@Vas6_9lyr;{$4!sir|o(w(EdnM=i5XB$n@5 zOiSZZ01Y|r-V!Q)hXwwFS8rP1p9Y%R+HADX5vJRJdR^(H`|Ia;mdcDZg7DaZ;Pf&j z!i1ma$@|kmEbH%7Ss!-++8BF`u3Y7CXO;6dG->lrWEiyFcV;|yBw7sl{qVcYD>M4o z_i95-b{gSG?i9+j(ne_KE=h7@M-kZ|Ot9RLKykV~@AYEnsD#HvzK_IW9YPtdJhY$` zsfR}<$xqrg@8K#=dWoPkZGHZm4$ZLi1u5Ct#jkeQ(cM$z@58KsuW!mMZ*Nvq$Oa3~i`=FzNn2k5TKdc@uG=qdd$Q1O_0CaV-GxbXm58(5$YZDQ^ zZ{=3@vAy#q@7>Jm1Eca^0cSsVVeRmfEu#<<8>Rq2O$ekmw5M)I*|yio=C@SMS%y8$ z7JJ0cd>rc{qZ;;s-Gfbpk%{S+2M6?0z);7EV5eVM0RYL8ky z{fSaTL&iWCoRxTKeAerZZ|5~<4*kDe6P8^B79A|9Iaq}Tbe~2&)C<<#gYu@|oGMwS zmNUif42|o{7|q@y z0fn{l05CRPG&0v1bvjY`yX}r;gPMF0t2LZw$6yo#Ic~?zDH$+)$EMqgG{@{Hn9)bJ zK&nNg3oV?r=1L=5gkU;947x&jbDwNZ@!$1rSLj=g?YRhz4iVh zY=V4`{_IAZVC!Ufb~fwc4gnNjJ26_c1+s8GSx}I9y}^f~rOfm{-8wx5HG5UtqmcCG z(t1=UP%o{v?jo|AU|aG}t?rapu)*d_Sr+YC11gCnu5BP&DE1!a;wsdY2Yv%|%{`;y z+g_e}1EBvc+oSj-EpHK|3+e{%`u%TA(4p29>WH3qKGDr*;|P=Z#LVw)Z9y|n|4$`R z5y&&W3~zPlJ)JuYtP`b(dH&;m~VoQKM*nz&_c86TOE z6{3F4xzf!Fz3JpzDC#kdMjnjz;#CHj#I1T(h#}UDmYUB?UFr9s$MZ^pqBlFbqPJpk z{417Xn%5=};+7fSzt&v_73)MLV*SOUuXn{}u1kxUcc&di-ClB!LkRcD74U3`Jj+e5 zD2*oXfNICn1{5>M`85v=0724ztBPFtfk~LPa^8l%1oEvu>SMJ5xh;!fDpoMvT>iBl zIz)PzNGX6%$ol&pnpQiDkR%blOArYr4mpPtRiw##-tNrS@a@Mwl0wR?*7!}&fNo*CEInNpKqhMJkVVIEaS>yA|P zbMm;14#eHu+YiVB1KKz1m)fh)-g^TJKn05)4z}0i?FJ&&-X(D7Oee_5qb_HN%?q{P zvrW^8h9li*psm-(@9=jO<)BMiH+~NJ6NVz4FJsdESdYX#b6k^&AJEU?X?EC60aa3_ z27LX^Gj*-C+Yw#LK;O|yLg+L+Fvl|B1+Zh=4Zjynu_o0&e@rBM$UUw(75-^g`k>UvHn3`tLXQK)1Lh zFZ(}ri?o>qcDyt~mWRf-I=%>uRZ^pii%qZfdi{6pS$wdNF+I82-BuH5S6}oo1ThNT z9tqh=$Y%I+73b{~^?pR3k(Rwf^&7VLHMqk($w6*j12iy+uvmqOSX#bBKLvkAQlpQv z*eSC*T0J-|b(N4i*x%Zjs|fF%TG&>%lYO&iKdO2A(SGqX2U=C|ocER7H~t5HgEljx zerk5~dG0UmL!UY3czMltD;E&TCNjzi;+Y4Cy$I=~6a{xLUBHEfF*2U$#vRwx7P~a( z3GVjMkul5{3Es+C$5=k-z3##&l&YETVH$@MZ?B$y+vv~5L$0TG>}r%>Yhc2P%;0AC zGz!sf{&<>SZslP@r^)cWgv(L5#=6G6lH=H?XKo++ z@y@#B@33$DK*1e-2jI4>;(xy-CFGvIeF~%MbAjOBKWdJt|2=2;ft9lT_gC6))&3nZ znXUd;#7=4$N=I)W`Oz}TlU@8@*Z*yb|EJ>pv8|CiG}iJiEEN9DvVT<4K`s2U4sOkw zGU;G;63#fS!%NvXzwRc0)9uupR#ID*429~8W`ajHYM?sPIv)vt3s=DKq3_CAGoE6k zOaTTLJza5$Zuc@#JA{68+A3kcuT%leLBhWxVIuY=S%R{qlkSU4yYTI!IXm8I9f(S? zx@`Ug*W{o{b&(+UfpS{^v~3)viTz;&MpYYbQsLR5W=i4b-ni7Sa$5e6tng|jZx(%{ zJXcCw>nh%4%`ffNLhVYvXy>UjjmXpEhgwg+{YdLXsa3h?6H?#Hb@xZNTa$1*vw@nk zjVnl0oy4gaH@ac+Y5MMUYAQu@&Gw6O$;mn-l2xiOTQKDAMf3HaKVHqlte-Tyy!CIq z?Kc0Fe5lcP+^0atIMCaL~4dSTlJwk zb^w(-2>$3&v1ohPU-J;vLMbXXEVa^fk^S?`_t>VZ$NT(FC)lC`99nX(V8~uCa6~IB=?4PG*=tpofoX_DolgHU3E(ZeADY>{v%(IYc2z9c-b<8T#nx+ z-?m?zvveaZQ7bBhx6H50(y5hv$triIwAFN_+TMl96Qa|j0; z1~!TmKb`*c_WQm8$nllOOA6y#odsQjr;qyP{X|FxU*Y7sIz_hoIJtI&o&$*jZ!qOg z&ILOR^gWp8)o;RWz^Rk3A5TBkB_8?I+q9lIZsz*uB}@0n!#4n5?<6gEMPut-Z`T%m z#s9B+{`a!~e=}FLC@_?}S5Li#I|6A6l9lB9H8$@istLNZXPL`}zwq7iO7#ck5}pl*ldT=n`&9x^ zev`SL2+NLB%dfdp{4Z@o>^8Nec6w<$ZmRG7?ohTm7+8)A?=LJc(W=OZhLPxO1%2Af z@&lV$42v5gdY2!mA6^b;b{P5`uFqxm;bk-$w6IhH}!wN`P}($hZck)qA7+y@l_LNXD^EmRQI6 zcusbOpJeT}K)jOePS$Gqrt!5uP{6RsR!?7&^*O~gl$^MQ_h%;7UBMyiLGN0pt7 zev37GO-Fj;>v2PJ>>X+QN1n*TGYz9BA4?cPD61u{jE#mIsF-taX4s50=U&EKngI>f z*80lNrvlzHmW8%o+yyLWcI|`BIJ`DZ>@zQ~X%W#knH*dtE%+G56o9iJu3L;xakQ}8 zUtE*K?8f8atJH;KxAyE1n|PI7#f7RU?)$C;kFU)L(M zEE4+TBa5Bnq(BnquHvLqlAE*{yvuJ#%4z#pdhZ3j(tS_6^rpErzF+J$30RR|)<176 zVtmYZo4%tf4|y`~RR7Ns{*}WcY3qQ4VZ#{LuvC9QJwfVzqoxDnY2vqsTgvtPE|PAR zR4vWc_;HKVCRasG(?;d%GP}$~LOzmB$H#~|y=x0go{!gwtZ~z{(Zm?hvEQSBS7&?J zVALDPr#G*Oq9I~OP5#+qS^cYV?sRbxQjXsPFF<2sr~WQ3<*?SxZ)tHdJxTh^#C;|) z^1Q{D;?*(gZPTrz$TN^V2p+jIn3STU3S^t#N%M9|Bdp4xRiCV`^F}E@u5it3d75g_ zc{*wOnRwTy%Vql~kx@?S)d5Eymej9bU>mvQfUd&~*&q)c@d75f7tVY-iPA3HlKt1{ zkm|#!3fx@Y=cgpO-_-oKFtWZ^2)+q#-ppk8kp7y>ZPa=UgtN1Km9A2&q~|FIAJu+& z2mApCH2=^o?l!nYnx_nE&mqRBKU@n5SV1A>8(9M*8a%%*?+XS-f$8${`| zA))lb>#(s9ZiLA)6{AR6dDF4Gv$c^Ff7N;2{_H0bLWbRsXhe+*U#1`z&NG zG7nu(kz?%)WfzD!36$>k4R*{dcD#$Boq;w*^%5w+OejzYW$fpwYS+;=H$v03-6}>y zPCG8dO4r)!75$vj)}~xvBkn}K+j#N*T_a@b*8#JQE8!~lem-1e6bl_)M~H+pIj*Uf zRp$&`>~u#Cg;){;o5f`=wxtaYKgQjQbuB0`iTM*LL2anVy!o|z{^%l2pJDUsyDvvd z&8gNKj<6D#%BQ{GQOh?qv#p!TER-7l2rHF_+CnYv&bC4yHTQ*!whwKqig}vq4Y*-| z9fnkuD5quCE`P=-TaCR=GwEXg@o)vARD&^lP$BVNEV`V>Mqk z@ueQXL?_qiQt8E%E`Js*cg$Z+CAQL(R6fGseTatbNj_ROVu^9KfLgVks1Vk;pDMbk z>9s*J-ldXWHc0=PRv=*(FQnNs4~K~>1g1rNV!anS0H@DvYqO|QQC;lB`iY?iee1VrYPUPJ}_;Y zN$#`J1LzMdeuSel$B&LHP7&n#Rurz-X?{}fbF{3YwFu(KTVOA;bn2(2aPyJf}0hNMFDl6({IcXc@u{;Ab=A+I= ziBl~o+vH-t&Ek9a%$2D`);S44Mu(RC?QNLou%r7epO4P{4h*MC4!l5los5R~u{QHQ z`A2kzhLN1~+kVU+852Q6sx}|QHK5~BxG-fWdD7C+kWu?q6&a%Po@V8Q(M|6k86Kp* zTVC8Tev4lTR|UIw&vt+Fd&sA&27p7Cz|R!fsd>%+AE zgWtsnxv-k)JQ<)nsq~KYEmx(fcF@+<)8zF{=>|%x=g!}2=cU+-_wHk8%W6?%A?(7# zS>skLIz#SDj77s(4%8P%O?sKy z^y%k3tsP$17+y_4Ir}LF{#(tkrO7>L2uL5_|19zefWI@Jn-M#*f7BZnIogaWbgzs1 zSLNjUZR>RgO82_LC`3<27F#`e9(SCHQAI>XmPS1s=O8V%yU}e1>2-Vc!b_Eu)1CbX z)qvdLevFH^OGds>zvk!Nmx+R$;dCb}g42Xh8QbT~s|X)kNv)ZipKygmD*3|6l6xlB zB3!v0>N^hmYN30`*u4P(|06=|-N&zTmd`^B*?4P1(;#Bbv^9t@f@Bqx93&MP5di@K z0VOAu43cvy1OX)~K_nwlVi6=m0U}vIax8Mrxkv?lP7&^R@ApP`zc>1g(LF}juWQ}K z+H1}=*V_B+bM`68$Y+Ygz67!C0Si?O#Qw}N*XQtskF!-wsHOH?S#Oq7UB&B_q^e)s z?(+i(oRAlyieHe?39t3VuiTe}^|L!J4{qn>S@m?>`H>mXEE#`GD-fkx3bS)q`lW+hGkmuQsRGBXE@CR!}0l-T<*dRTs43s7ZoR)7yuAup?h*bga_ow^J`&s)r4T>jtfc=MQLKGt8jz$-y4{`eYY zAjH#+!pxer!WT!(Zid74- z;g5oqzTyaNlAe96q`Omegyh|QK@)dHugi3Edp5ZEa(&%qC#ld>sbLilA72ViT6>I1 zB_0H-wmtaVK;f98!ezegmrVQst(rmVmD5rm%xkIUR5W$aFaUaPGKO1z>v>{THWPS4f0pCRx?EQK&3FXRJt`WLj)F%WIB8pB!N*(e zGcz+ONw4TKz1{@f2nge_XDD!Ye{E&pH1&y!@bXQuNc$?naTBXivc?gvaG+kII@n|+OaC7e+j@Of%NmKXF&+DGrSUM`ZErv?EZv`mhW|U49rAf9d z{6QYK7s+QoNx|)&W!2*EA_KBKKsL28Z(aHwhlb{jX-jWV4sgLA9g(7_vc0Nj?Yh5P zyA5|;j)radjyeHpC%~@Fjts3r(OFdPApvBa4#CUF%$@J~px{uEnD5(%#TDfl@u%_$sn4URjDRCqbk6mPZGgiCD?-k&Gi zF3=Q457c$B2_A2M{7Bc(fX~95x;6Fl=WUWy_aDQV^lV8i(#MNn3(YWf$8 zG=m^Yp_~Xxp^Xr#_`PKYw|%;_QP}n(>`&D1>CQaWM7!R?MUpKBXLb(3RJI(Pb`g~H z;wToS6|kD$>m71_?uHWD3?=BB5rZA{wPjjx#l`0wjls9t2P?L_;Fi-I(Gur0E_oj?V=)MbHr&E-9uOsFtd3tlVl)$9-LzA_sTU zJ(?)2t$#`gsDaxx9S!dr)*tY z_c~@)YqI>lK&d)a$hC}**wEBW(4xu`;q|qwGOL8r4gdGf_gJ)2-R&K75u z-EN@L9(TZQXP8>3>!h!s^IT`o-b7-*wJ%E6r}SKRD6xdY8=3XqG}*qsM?zjnOpdhNsvb5}uI!!XURE?1 zt&B$P2vs6Iqva9#PS&pD3;mre^Ue?tmkT?oeJcju@v<_rwta&FNZ;OCH-7?9H}hNQm5dSYnkLrC)fPFsgrP zdl%ck8W$QgR+Q9$FC)430~2AjE<3vse~)D$J>@TwT?vS=p%l(q(RhU| zuoe4fc1cl!{{m#!`|jyxD)e^e$>yu2*Z=ZY9leQ6YV$GGy@#cIxZa7jmsS&bElD-Z zao!U9lKtd1ipAA zaxvZ0RyRS)LcH>BiqaJgZEc2#=1zu+#}SW7p73KI`nzkcuJ3;)OLPH}rTXTwnEgR! zb{A8dzg74Z=>{J-wxahtY8`@ZvQE({{esf(Z#BN$r+lJX`XVki)`F}5=BFnjMN6sF z>37TCfr9z1+h`86OXy`6Y2{UV2)Lm0l?Pm`jUqWXf^4tL|B2spMd(W2`=T#pcteRb ze0G+E!j}c>)mGA=ort2?5+C9QgooYhZh6wJ3w337&UH=?b8d>xudT&C%3xocT$J!> z?-$#47K&~ae>;@VJkNZgIb~yMsbVts{@7nzlPd=uv)sQ%CdjdLIXQ3V3kcCYH@zgH zYQjafEp~&5E7=0;==pcG<1*eg6hkFCImt1|yxx{SiHwl~pVUW3z0)cte7eWmn{8`Z z#t@4tk=g;q4ePz`;txPuzO>Xo_aGT#0`Tw4;Lh$cAAS3m{sOItO_26l=o!wy1zI(rkQ*3&-jJfBw-yldfWk5u_Tcvz@fx=MREN(fB=HiH`&OtWZX~NxQVorQ(e&wM1do)c-o~{tB8+7en!7HEG zrvuD97x3npg??Z2F`K`J)W7W$KU&AKA800dZ@?_4BSGU2!JO%I0b=GOQ@oJX*1i1~ z%VqKx1FN4JGpiCvjp9_an0>u#n4mZP@)5suJBQa@4Sq2PO2LxR+0_YRv_#OLJ%8+H z{0mE1`(ihZ>wej<7oq zJx!aF^0D<`?SxZx>1hZ)CV4MU=uGBTsRly<3r7ELmOyqXf;rO@ap7`i;B+M)~rg z)x4j$PBi%S;z|OWe^&&wP=y{}&@;OPnA~nzUrr89RLOIq0w&i4ji2MWn7L7C?zlg5 zXpX0u(_*YMfa_LM$bA_p7B8B}F<#y1>(Rdbe;x=L`~CIfPtKV_-3aE3^@MBdF{ zByWJ1-QPPF;J0DbvS}6S8;&|4epZty;O`j|}F*N$BGk&FLyx~Y>_^_erM)=Dt0dH6D2tzF1 z-m|B=^)5D)oh&CH9%f^zXHRl}!lPI`IT!$e#`@W7G$F)BpGA2gW75PGAwNrCZFZ1OFK=cV#=wiD zxrV>l$6{_Cawy7T=l)H;y~u|Cc97z%ovGj(^WL`?eoJ+lNevfrtC>yv_?C>IetfG| zBub+tE6%7HX2XJb41d$8=9ZiFsrPxny|N&@g5bz~k2fGKJ%1bjVEXGXXX^wAH&m+1 zl|QjwgjaWEjxfSKA^b1zLwFo7Km1i*sMt=Oy8BHFXL_}Q`PKC5l9hgoZ8kwT z9yYO3ObD08R9c+WFiL1Uv&2PU6!v-B=Hk@*uT9rENxL{JC~mpcs4^RNvk_g;>LE_b6V~}W@yyl#M`E0!# zq5Vg_Y*m-)3q@hp%d%uLQm*at;R{15LPw3B{z^9kHKuzFQyH;zql^Zk+R34E?YP=i z%bHakld;;RMU#YV4R0QJ&5o2CP-~L-@xoyILX+@Pz9q)c<=z)Qp?aH==BBIqWH$BW z`T?Yxt5>^~6?0eW`c&nq^rFoXBv+jCva&oqWhxC#Ov&g|11LgOMXpqDtNDj7J2cSq z+ICx7T7$Fn6P9NoRWxB{7>En4j8KT^J0;xN)#EYgm$Rfqvn zi9QxtZ#;gvvOlW0$oYCG6qjC(4!y6x%~rj#og(3|h)W|gNUJw93 zFD~>=y3B`rb3_VXn`Jm+rF)WfCg7z5DzL?fGz5K;OUKK1gVF;A3<7pO+Z2y-x>`{e z(rhkL-e(x>b0{iGeK*nE9O#B#P3_~Ajs*&dStjDxzC&4Y!B)9x131BaJ?PS0RsxCf zb;c{c==7;>UZ_QKGFjgX3!X@3;nA;#zgwOC5-ho(Qnp*zKYWMqa8MEqN8>q7hC_k# z+(Ts3vOQL?=LgYmd}Mp_e50q<6N-|QC@5y;vkFzei2`*Re{C~;ie&N$LwY@q`UA%1 zTO|9oX)fBZY#wZn19n~NXc zhX?UE`PPb)mp4rA1O|7kuv|$jd-I0Qmsuu?B`*zMDWP)XqoXC9hw%9(;fJ^K(sf^c zxsA$j35B`t#4~WhCj!d)zbBhY(}U0zw#WOp3GD?5@1BzqixzB;yDr1YczW3{|G zIyJq!WMt22Qo@d+gCs^<9viW*!N9JG{@v*^Ft@H4bKMqErXlt+XAaL) z*tFzrYa~id{^ArD(v?kBhi_YPAM8|Ql$6X&nU;Let?jC4?McfLKzS9~1a*OUvL^Y6 zl{lzeC2dRR7Yjin;Q3lT+3lH?WBzecn@pOaWqfXVEVs+T&hkQqdu=YuMV*@@W!-QH zZdbl(NYJc{9nfqR-lBKP-w;5h+Kb4$OpCbd@mB)l%6};xwxrRRY|P?3Kok@^3VwH6 zkNm@*miT~R>7%gyAJ;O;=aM;a+3!wQ7fk829i?8#3-ys@WCj&eF(_7r%sluQgz(sb zHe8!8PL+#$8~^QXMw&81f1d&WAC4Ml;&B=1h~q^{Nr{|%bK1;j;$IFN4j&O7A1&D1 zQL8vu*frG4;j5)~59eI|SyMUq{LW<7iuY`2U2l56HOVNha4i!e|z$$ zUDoedK0{KWkshH{m~%UjgQq;WZdy|we~F?1=C9g*YwRmKZf>#SmamE8TxIeURiC-t5`awS2xQ7D0`rJ0dlY|oN*Gx*7M<({{L99&!PXk}Op!|BOP zGN74htlM#UL(=o=(&l^hUdQVjRnIq)T1aYNRkhgR)@j2H8`0m|p%Qu7$v;fc-6F(V zE2K6BaSt0+h^9a84_?BijP-OLc6SKi2&1N$;q=R{YbrwDVyj}^^%|Tx(2KrbT6I4o zj^DOBH=SRuPjREz_pxb}d79oq-plxF4fZRa7=#@@!cvqf{y+;$cwRQ_EPbI*XaRc5 z!WC$^eBnb!?*d{b9QDLj*^r(P!O=_sWD=|h$(wy^x-{+Qf?FEuudJ`FGa?i)Z%+%6 zt+mF+m%Yn+9}`H5VVgGwiy5dWDYy3Pn<;X9T2ZNexy0S6@*Pu!}MO& zMH;%4sfsXZHZ%`M+QiIlX1+5kCKDPF4|*`bZ1Q|?TSYs_Fs5)vTlta@wpHk76tOIB2O7bI`h;*BMiKY2IY5Ji#nk@j>6B_f=71=R4rC5RgF=ATu(Oov#;c|f zcbj4+J>>FmsaUF#?@%CRY$88??0$c1hf$i#R(cInGZDW#$M5*vmk9`-Zx4)J_svWL zgtkiVSB4hR-LoYj({m1ms3~hzMi#xkT2>aEsll~;sDU`5v>R1^1vVBg^OU3_)Xxx- zi`5ehp4xb}-#*p_KN!k3xwG%t)vlPo)J@tef6eI;6uz(>Ufk8Oxc#06>+0OoJmd;c z^O}xu7w5_$^f$8I`i94Ms|L)r9et+0CO=@)L8unGn>>onenDv|@#1*lW0I%#Gp#GR z5P*Ss$CaBRyuXzmN%{ech*nv}#m|ZJ+W<5&&ABev(o8R3c-QJ2T2xN9h+3o25Y>&V zrb4NL6LX_ybjPDrJu8`&78*`BS7PJ4glhylw7r3BRF4t-!y$Unyh@;V=Lv03dy#}% zWebAm*5cm9KmBi0qS%fu469sLYyvurR*puF3yZzA>Vqb063Wu?n)_^*J?b|5USzzR z+hlY9@%;@qCHCr}51ErLE?omhh+l%C5VShLd?lqQM6qB%y5`eC?Te7tb4lu3x(mNq_sWm z(&gpgXCB3EeRd2yd!%?B8eJg7j zonsu>f-4OT=`|l!RN+lvHCzx|DL>NprP2V1cL4WL^lgS=^nJdKBDN&mSfy`Mr80+K zRwMM_7OLwN+QG;b_GspLDN(klR^{Go7Mi6gz95&`_9(QH=tNF-S=%jA!%10!X5T@E z>#!f8tMgGR4;Se!Hp-Mee?lFV;1T*JkxlnD`q>tCgoz~?J1`#%YldC7t)PgXeA(?K zOwsyOqNO)Cj!{9%RS&dV_+}gX~a3S0EFZ*^x!20`agMIc`}3%#xP02@hi1@Ujln zwfmcFC`Mrlw{G{TN>BBQ4Kj%->AINzOfEAh`|$R5w4uYze+(j`p~%|%kY9V2>> zfyrlN<3=*AY&L0hHkVM<^y28=ikAyzid=6Owo`hkl%m(po6JK|Rgp_vwy1$o~XlCBBx=|%1)X+>3+)GOXY z?|(h~)ebE9!^ySsw1VZ1R`l`>ZV_F1i$kJ(4ZMS1_QiR2jK#E2?b*WD+ju@DZHF<@UUWfi))K((qjk|M0qDZ=Z$T&_^{jl^sm5 zb>Tj;L`NmN00}@CJH=1B{567a1MR3a#C6%|lb__sPRGkI*q+_D?WYPR)${Q+DxWQmrKwN)uE*lQkpZR8DYS^$ zv`)BAoGZHOjXb4XPUT1IG7|T1i-LK%uOW@x`W9Q3Az2@O&J~kY+7$y=cMn7FmW=Ri zR1*)Fr+5OZrh=6l)|AkQd$WU#3S7_Gr;aS^J@2BbBf{J8{LQ|@SC^_KSEUrgR*Z_n z73-nGGuzIcs~6ZEfK^6oNb*>?oK?MNklfvurHw9>=1|01$Q-WGe2`s_RWmT##Fzg{ z8xV<2{o5VHrlBt8FMdE85l6;zo&WEts=_S{LpwM0uC>W+`cvSBlz-B zeVO-TWy~T#NhiC`Po%L`af!ghkijD?M!nCVATsFN^AC13um(M?u9-um?~Y&M`0$N*)MdF0!T(lhMA$*_>_{l_pR?v-BD{^BTKP0Ep;m&0vtyzNKwV zczOZ!=$p(Ic9qHXCS?kH*fvgSDTx^3iudCzjYb|(Hf3e=A4AgZF+!1pp+K8R zG)kzRw34s71k_AC4`D*PHKEvw#ybIV^~|>^BR#i}KAERGYC34}bam0pZ3$eS<3avl&ddYz_0Z zWw?!;{NU!zBloqyU^_c%_|T6Yv!dQ!uH+3dG3ppTmk>J#a3`iO4YO%8g@+2q3N~ys zVB(I<^1S0yixqFdOz~Il3V6O*SI!pP&GB%(*Ff9;RYQy#_|C(%_CL0A{-^hes;wix ze#ke!>+s6@j{JB}thlJ0oTzZOV$IZ}pz2Ise?GaOU$Xftgd0TKaoE6iQ?4eh)NAD9 zm7i0O;(q>*Ho0PW*$V#CJn@#o|LG>z=RGN)=^%c#6hMF-tS>UO9G{=ksycl3H&QpU zR&~J2y{_7)lk&We+kG9b_`QmEiYBWI&VjOHoKN(-Y;$E(0*+THm{r~=0{*oE`)wdN z)}KEfY1iTkCS8d8a>4yaUGIbE`EICX-87( z{osA7w;c%t*Vj%+>lNJeaH5N7hYf%4%A#%AUQX8s;qON5zU%~BKmVB4mibrw0{^&f za^2~z5EGW{6;@tE@EeFCs1?uDe;~5LM=^MO&sKIOy-|3kOc;_Xa|kv9e5in$>_HUNX&+ z_`1Hg%TWK#AKNYd>-{_VjV$U5*6IPFZkK~4TFQbe>K3tS7OB3FynXb!@qLG`YLfuR za-9m?`R2zX>pNByy^Z}x6WYXDKH44#m>zQ~m> zE4Og(_0MWLr(S|fe023q!d`Su3|Zt&78rSRF%E57% zLwb$WTg!a2I_GVBZ^~HZMb0|)Rx;BJe(io(7l^tg9MOc5V*F6a2 z_N7M-I|P-)WjMfjw_VyrFbFO^@V>Z*PZl`FpHG;xN@V%qU9?AHy&wxljhLHzcxvGJ zq=dWw{~!Ioh6r~)`vodVMLlHg?k^e5_wVY|*HkaMRC{Ar6aU%nP*WI#5HD6sKDOHb z^+7={vn<9CJ0ILPE>>+E8L1o*rsFl8njKiwq!z%lw)xo}H?oQj0F=KRB0rTENv{Hw9HsE%IgD`xXiOD}F-B3^Ek>^_(pkN1 z<}h&(9YvVyzU>s?wL1w0tRwO;!x%og;^r zRhBpJMa9~KdUyB403%oeffWa1_>#nm*Vic?DgyWtfURMI?E~a6Mv1{C_!YzzD}YhM zy+;iQ^VwX`a`Z%JU4YZIQ%jHkv>Zb$E!yLQ#BNfqV^D$j4Pg(#9E{0f@->VZo&+oS zl}@ouKfqP&Y7j>4#(*VTD+a%SHHhD)0hEl^CBY&$WZNrW?FSABB}|}cK%f;A82DlT z@Y!yB?(?LAFg|NAqQwSlOu#`iJU?JiyxJ5{76pbI52VE`%g>A(GU0# z-x5#>Kg;JamLz~>!3iQ8aCFuHjHqlNhm8pHaZrE)I;xoZDhCk7orsEh736+42&1Vh z)$QlQYb>1)pL8<(8wya?XEg2JJ}8T=1$4q%fy97!0Kg00#(;Nk0dxQJ1L#x$r+Rj< zk0l0!iD~sb(u)}VkEQ*Cr;@?bgyNz+Fx+bak)m2K+`|^ZNMHTJlv^(&Wa_ zBni-@*#IJ1D+327k-%i0529UnnkC$bU<`Hz0DF8J#?tzqmVIZIeV_lftixC`{=;&u z*bVv97*weC^YbLR(t>f;ateTLxD?aTSTA@`pJjf$cLOYta^S*h>>{Q^#QpitbVA(E z167GUAHM1&P{Dr=|8U0+Hw~zI&5~hE=L-Vb&aN?7$v(agU@5UrfDN?`%cDL2j8MDT zGeWDY^h3D1%pG5Vy2qu#)I0^Gr0onnNy7Q?s~9z%bceGfL5$84uM_amAWaMCx@*B0 zfe>IiDJDb3kZNE8Y6TNC{0T!530TCKflD0zIk3)oD!-nm@(@G2W!gV+KRw6&*bSU_ zK-_Q)LzX)KwCq2(d~t5c)(oc7lH{M3X6Kd$=az8H=n3=xSs9Ra3!P$1a9;5CKk3T! z0Pvd_;8*|Y((lyeFv%NWz5{@J&>4b^mvn;($E3y_3vy_z1H}{?=K)w~AdhjOVB;RY z`VW4pHO~3zKI6wD?KS9d_sKsk56-caonvXQ#t3-*A0c|^GcXZILqYVo(JW5IsxhFWl-Iyud zBU^;M3^M-gnS$~l9^AoBY(Y{j#$p*^Gz)=@)Sg-n2Q(EWV5s!M49xwX11C%dND9l_ z<5}Y>BK0)>umb8KXeHE7L9Sg;fhN0$)$R4J_}k+{9-WVC{M0V=t*8L-(l)_+N@3yl zwZ&Snt$W9{R$H3 zZV%FeiH-&05j=m2*Yq6kYBPX$cl&R+J31F3YwNM=pCa2swY*~DUO)$ke+|zK{^&>H=C1+V-wIq2`RucMuw3L%?q+s7$IQLQWHZwECeP$gW+lBV8ZhU z!gB-gj!f9V0bSAiW5#W`1R2wBb8P%Q+ms`UuZEEp(Cf#10n3$LtN(!9g#}qlI<*`( zcm@dT9w01fAi#3hKRtNq)UlwfM1Z*m1NSkV#7fI#g4m@#Wkp%8#GVdV?@tBz{&tO9A~UZ)tq&2PF5NffK{H!K;Xz_7 z4y4-VEE_#%+1RZBL<;K%YYj{Wh4C`*dQ{<{f92Mry^&Dg?p&Zk5QwYZDId|%XMDte z*qaxzXe+k%DU<;wHRgY(Nw_4FJRi1axM>JGQW?At_)t9FCOF=rt2%ZeIo@_SUSB;% zk-+xKcP+ri;>vZ5?t>kOz$TAv56h3y4WrROowxv=yZ`MuZse4;!h;Q87?-%3u`j?= zeC&E7D5w?NvtA__e^T4cLg-c)*tRx-Y{Sol$Leq-4 zd0(uUbC3mEB^oFJ>@%T=oC#&5_C!h37Z5AAzz!o1OwZa&|4RRu?BD%KWJ4j9o@u!O zu!EmgMLFA9Rajz#WQzzVcZeVWsRN5=SHVzFZw3&kF6#bS`_`U36S_kABpsNGc;E_q zxbGyxB1){j9e2 z>&g6bIB`C)IQd#&0^|nD(dI;cxd$Pq@*CU4^x4g9%@ow3HqfGX3ILWJtWR@xIxf(b zU5U>sr|r)H4%mB-fv!GQdD=+N2KFFWJRewr8O+)-Fs?_`$$Zf7CIwC}K-*ih0*UU} zQ_cynZ)wMLeBk)R)BjFB!<6(F{(cdl$gD2H4tg ztjL@ekq64=qz{x{7-WIJ0;f!1RW3hss=p;fGN^js+QX6QjA!kG zK|Z+Qsb%}Zzmz)MdS1MVu{Jno@ok+5|4e}QAk~YffJYVvSx|bX?x=tLH`A48Ojpzw z+N`vL%LE`qVIXyw3F}NVgpd;nf_<;zN1@HgCve>X6p5?qsroHBPo}^zl2a()BBKqQ zUV+^bV3Pi~rajs*nrSQEkn1&fQaOX7cB*MFWW?zlJyuY1S|l4cd)pLrPr<~u{TCSF z)U#Fu*IcHMJ#*=FHtqlIlIGl{XHT2cl~YTP_|rU}Wl#aK=6cSLT%iq>|0(J*mH(=F zN89rh#trfjfC~_i$u;bM@go>>8YHkb5SZ;a)iwk%^Y5B>1N{NL#v#eRrR$VNi0jaQ zp&l7J(`>{y`?gT9`T)Wq71jT3*?VTG5^vvD0LG9}6b8BX_KFRxs;#?JlQ{o%$p zU21lT;>_#I!1Gv70wDCrG&?t4dOEwrdTI(GeIxQ)GL7+N(o2?sq_~-7z~SYrzR)i^SGtI%A71m{9ZPS*Q_p{RAzR|A9&i!dlMI zDyz&qBs;M;e^={0U9Ov zwep$Qe*8Bg%(dy#Jw`n}4+55yWgM_nYeB;=%YX(hePaFTa0V;a?e75MiZMCRg0u~s zd)03Mei!2;*8bOjd)++I7GAYY6v$RSl2#khS8{>5ogf+L{*asxdDLIV|3g{jkeIh% z23qVx9gmKrNyZP=;;5g3rKBnPcM95vFqvhAR2iP7YcFHsYQY&<&Gqi8K1$&k9T}Ok zCz&P7O-kj0s^ytMKSo&Gw|_Ui_s$lWrK)oPwUC}v!6_=^Y9i>V2rFC&L?Sbqsau;(xoMmfT!$<3!s>T1o@cMrbe=GNl|LWrZwzQPOXl20sCfg|4nAMq9 z9P`haI+a!Is>SL~FX&p=aI|OYx46pdVSTSM&LuhTdS@Qgh1J$N}g6L(8S{7P~ zFwKSB3L8V0&BBBAbponfva@g-Zy${Brk`jaLg~ad&kan1)uA6iLp#x(#?zh!0qgL; z{LW)~7RGNdo~4uOsSL+L$o_U5tiuI@DuV#1h6jQ&M;G+bSDt_FMZgYjQPCtW7K#2nKT+2V9T-so7%qz-qfZ}e-Q zkx}AV@9ub*c!E$~6N+Kki4Kf4R?h7+I+lR!QB|Gv@P#zA4yM|REkZ&4 z3c4d@CzRaVt9#BA4ABH435!4JqWfDU4?*Yq*;%i^P|}9S9;`=JvFi4naRM;}%zwL_ zv~o95JkYt$+)xatqsZb?DOW3tTO}E1qQj`8uChca+!wYc=?gwC6wDW*(vAd(K+iF-efyD}&nA<+n_VjG$ z^61x6`g}o*gn)|vz1flm3s@0!)UlW@KDcFMKEhwzYr+@R-WRW zc>AI8MGK0`{lWYmI#fz#mDO4jb+mv3b*$iF^U-t_|7ukdmFwYL^GHhwe(LII3ED^HUbs{neIQ@nI_$hTbUa<-HossB;&tDe)X&7Bc7Yu=u%Vae z+A4#G<43GuYn2uSqX@TVe+tBrkCVWj0s7bifJm8)Nlh@i39S$00v?pJDzqL4IBt0! zf7HCcKYq*HaoT6EtBMy$(BY_H+z$V2-SA*l5o!avIIhB0dF;>tOGX?HAI-uXZHF{q zF3tYWR^4*1&FUO=><7cD{^&v=tSpiso!k9Yhm94n%tOP24l8c96F#+lqrA@Bg_6+G zgSw`8g!SZDtOJ1sdJUB$3~(`p*c@-qf*-?$_CFfa2#oQrIB_NjF-PY1UAhr8=2hFO z`MNuNiF}7K*+nKXVd>%3=t|-zJ5dklRi3Ex7W&gnPz?5)s65QjqaJ*sEbf~Agt<2F z-YuP*Lz}4t^$)LKTlaRNMZ4>L8?C6SB8_1+E$NUMX%zq5`XReHjbyZB8pjk-OC+=@ zk~z9g$7M0R@iU3>_99!hBmr{w2_&8j;-U0(jup6&w0u&k_X2vzJlRx>JN)Yj6W>tc zClPj{$``Pm#}5>#SMv^#Onr;E!Ii@0(fH=BiFvt$F>x=`+^qf8$MCKdHpAv<+VL_t zta&?I?M(8@TH1wd94_qrhCpHzAA|R&F6gI5pzk!)PnXsN8|Vwp<0$+l=QcckVKalE z4p{Kvh9VD!M!(0G@~#p%k9W*;XPaLwq0Z@FzAoUN=Wz5`fZSpffVO(PRlHw766oEt z&sK#vXn-xCh^H;5zTS!|$R5ndppWo89s`D?IvMhQex&`Ek5fksd=%@KeGbi1BZQ+k zw8H}yBpL@+&_^0fmCView-+Oi2Q*_)eU3CFWo+s^v)rVokMYv80mE>19yh!BBRc?>+)hM4D$xInch|-P9m@gH9o#dN~j%uNq8M zly*X+fz@(#j&(XJje)2_c<4JHPO5hJF!GM%JtQH!jl3ANYEy#M&uk_s34gOq$H<%0 zken0cki#TMQs+9iOimiu^u)xlZ3WII2u8eZg0r(T&^pBFUbJ$w+XRfeJE2wv+%LM$ z#aTIgEjVM$TXMQoH1FklgTCN^`ll9(!oKf(%UL_l7rj=d3VREXtJ?zyBot~1CB%wV z<}rLj!kXy3nbhnpYg5|FrPK&A+{~ja0Xv!D$^w|_n;k@zj9&a#Ep1>+k3<%ulRv-B zQe*Y+4P33gVWIoIP*=Yc`0b;1b2;toQ4QDQ2pHl31zT*_<|0;}?%(~(-t8cjQheuQ zxbXAe%sFZVGwSH1JhYPeR>-qt`;Y=8C+ZD5xO=N4cj7!CiY8zFkU8n0NXf^Ky=Hb| zVKTlB+;qTCRxMUV6%gp%o5s7g9IK4vt;vXn4-Nd*$7j3lT|xcgJ`lpRa5M~TPxWMFfn2f+%xaKCHAW>5J-pMAkM#b89>TdETWO|$c zw3~!?4~`G?a#yV28}W4&d!?prpudb2t3f2kGD=HVuSV z5l(lGZ;w3vMQ|@d!38CszD%e7l5o@Ak`7K47rw4%I*_TTt3v?sXb;0E()ojCeWkS) zHff-fVcWi!!HRK1UA;ja`EA7-0g)1zCcflwlPP6yUkoN-hhohu-o1lL+VRMkIE9F) zdHuzXTKSzHRKwYHiUQ=dN!u>|l_Ii*X=KH)*{Yvyi-eoYRyO)ldYqD!s>hq7=+`q7 z6{Y#ZCb*F5V=N5wTkQU(tqVR-_Nn7T(a){6n#_Jqd^|ax0(L{NYq5MxlJ@{jenP^g zzC4lko>?X(4UC+>Z#UWYmr72lO3rG4e7oOc?J$`J6rl!@jnzn%3gs><*-)w0v{9B@ zA-y0}<;$Q(Kva^uB&CrVV#gjz7ENSVvT_W}(ls~0qDTmA)W;Vjj`Po!ZU~=g4#_*} zY+vbKuK#H=iGn@9tB2CC>545*j!JVJ=ApxgtGU`%A2l#E8S?gn08;0 z@rNnu$a3I2UzrH7|D6{5M+->J+uuuF=-_)1d?qOhHqI8>!~=A94P7j9%Q|c}1r!<` zgeyu6guW`79!m1vG!BF_A9y9M4gVHePE9MToL!-`i^w~&w&)&EcIzgp6w&NO-vw@T z2;SClLkF$^o1Lz5QtO=o>@6WbM*$VF6AbVG9HHI)ypw zRdirUF$O>a)4pzj^V7q z@B7Yo&b9y9`&|26`!AQU*7Lqk-Ov3z?^*yET3iJK;{kAacle*X zz(4QFW$$2MK*VLlMbzByY!5%SSB0mXIy}pF_~zWAN76&hGA;eKwQl-L&sxzldD|iu zN)UJTqohHOk-&t;`&JWma0FcZq=k-g;7g z@Am2!rvLWs1;$3}YyQ7ip6|M`Z?8U+{ZHSvB4Iwg%~ju4JTW58fgc_Y@_!O%HD-;i z%NDi0FvdTre=GRnU4lGU(12xCNX=Q8gi)}#XoI?X3@P$$SjJ7UzCJ=5L3p+8o|gfV zQ(a$R*WPigpY*XyNvCb=ye{%JtWY|+z7>;QJ6{}<^-`l?V!f0ugl#!ljs9h5`-@=l zT?h8GXUS#Iba4B^thS*X>Ud3okh)TD&W4$AaoT`>_(-6I*$0K}#>66}CB)!(XoOAmHJ>Vhx>_$r$y2iHF`IGOfUqhbkh$;O z0Umkf!07rd5P@=B2uO^V&)!m>NSDr1S^B^e+Mn+4l%R?SC@s19k;H)`N*dUP7&UPm ze;*Rt>$G?$ji$$>uA3mVvz=m#Wr$=}VZMp$DSG91BbJ&|^dWCUhIfV|ko-m?TKEb- zf-_I)>@E+HI09#%0l6G_Hc8bS;XHNa*(wY~WLDj_{7b0j+Gbyc#_2wzs&5NY$;KoO zN1|nc4t4tB#KaOy+K4*%pqHBZfOlOyWATb8SYkPifE%6f7{4A@%oL(+VLYP6T-5G3 z(q7lkTZxRDAw6k5UmH9CyoD#x;~m$Hl5R78IwGxhSvg~~Y*VK5J8iEtAy5x{#bA@H zP@;&=Zi?WHDbobg?gL@hpn}9oqk>Ai$00yPc~AT>Z)0^z45Mq>3;I} z1w~A6SLfVpZq80rwV4rht{Hr_rxh?w{>JZE;%s$X4Tf8QFJFpJb;J%eN zbNu{qhhq``QwiM^93!{spzv3Y4RKE+ZJ*Jc@1%k?)h8T+Eh^^F>8Ih=F#bHW)47QP zt!{1mgW0=GL)8{8E1i;YbDWddM!{OS6DW;l3))NhRMz!cArh=m^M+9X4tC8-(rMGN zk=W>T^SVVDd$jx8=PD*CV1Qw{ZRW?vCNi#p6nr^|wO`G@z06J&OpV(E`%vO;g{x;9 zra~TOChg{5BU1W`fb1`ZM~{;_+sV6?mcIqwPU^w!^-|)wO9KmRPs=|) zzg!G$t`;h$Vf<%yLpqV}fW1h39FOxY_n-J)pc5dqeQ!kCdHS1(QKAS}{XqRp0c}X` ziqC#v7qJ=uX17TDj%pa+!lT->u;2;}Tr8nTJ~!8Fl{{9a;O)q-VoPZhWfs(D##P~gYUQ zg=-1(TV)pG6EWQ$3R(%K@7bONiY+lN2U`!CDk=l8@*{XPAe+#s?rvGvH4e;hsqi1y z`yDP}E_EH2`=}PQ(yDoCZ7WYR?%!@yde!PA*zG#;LU|o9@oXilvU=4@Rs@jLA?dZ6 zP^U*Tut}j_Lu2M51}l0Aab@>OPtVh%&h)bWsYH3|U7QAq4hesXWvBLpg?ea3tq9t$ zEoTRum+keycE5pv3Sc9OpIFV|vb$BBp9jRZd6__r?0=!8F>_Fj7Lyo4i-RkB4K1ze zK#D?#8aKMtYC4~WEpZ48T4;1y>#h63=`Th5sYH3C5#RT;Q zapPW~pLi#8mgs}hxEX%FPxsIi4z10j$};7xF4bb{5m!06-66&W01+;ylfmiN=O^ge zkAHS3Sy%Kz7f_Z!O9_QiAtyeIH{r``-&GdF}pGzRi zA*N19&4CKTEUi#Ht1|aMlmgWS>a0CdVo{jPr@6$fX1n36fBeTN+YBLEoj(k(hfEAs zo3fnGnL;(@`6jC{T^l`1FRSk9mt;-cnSs|jD=^V9MgPS%YR-zR+y)fCrshJ7oknHU zSy~g$3AupBvf6UZzG{~eWJ_4k8C5vLZa%R*%WIEr z7Eg}Ra=N`pg^{xtksz1%C+Eyes&cY@Cgx~GKKE=~Krd~NuGE{fpC3aY3nkavF{;-& zP9PqulYZTu2Ae#qgxn$QOqk{Q$Q*L0M`+DlK4gXuj7s)!GA9^hX9)4gP9JKzm!ohAX`=6sO`jv?9&m%b-fB#91U$X%c>CK(VDIzHS>cv;4t(4tM&E~-BuDC%`8yf^qsnE8Fk4nr|*pD`nT7 zkde&z6z;_hTHSPbc0%a356e-fuBGKi?>R-Z$9y=@LV>zh3`LxD?uIV^xx|EAPNjN# zpR5GWS$U$4E>Y|JS(;2Qj{A~OwubOfK9YAVEJGfL;Zux7uef}dyGKyQT^EsLH*Msj zTbnYB&L{IQ`%Q_TX`u9r@@&^S}yeWaU)($Ek&39rMd#bQuU#8JnP z&q1&dyzx6oa^+=*M8{)-{H2)PeotnYHqV545M>(fYk3)YEsFVv4-+ zr!}wu?V>ztY%e8GC6&k!hRck)?$Hb}8~kJo!Xm`#^UiI(cCb$zL{&q$QV~suNeh@K zjl%X9N9Rhy6UYXFw`|`i_r=IlcpSIO3E9o!nF;*g7G+rXoFBrYgDxExjdVmr(zcD! zZNg_0C!5e+*nzQf!|*8VQnF;Qo(6~JUu&*59k&!~4jrYwI%S$)tVPadz0^rj%xSZ0 zc)xL^GQ!z&(+oDjorxIOPiP|%`YIAk0eQ&6TV()Hc=X>JdQ^Ln&3Q$ zq71Lu`rwmX@(%35L_x2>O~-xS9D9hVuftKJPu)@S?HYQwYp4#4>6)`J3Z9|-PLRIA ztm$4ER1rTsIwn0%IxhJPNlL1B?B=V7tECHYzg!bi%eFhbc@ z#(fYZFCOnO?H*=@El<&}Ums4=(6ZkrA`m@wMCAbZ(tXl2>pdx_@>*b87IpZvEz6K(%v z;A~S@an)y4gR-|`BqF{lEX*S*w9=e18+DC`dLc+cNp(oW#iN5)dAO!*-&-#MRF{v| zm4sx!=Hi~s9d-4bk7_RmLoPUvb(oj|He-$=0L&j9KBH2Zd10&UT#f8+oC6D$txsZk z-LxdAgBM>2JsZl3=~i8DkTmfsSd+Tf!>G$^*W}>o+eACM>AZ2w-Fb82Oruzqp3TO> zqLeI`yfdJ`V2u3u)8{@YwnU2zy=NVDg$W5bC9X`DQIJMgdfEO!g4bd1R_}3SA-Rtt zKw)|lWfxrysom0>gUEclt+}j@fmWhDrCX8X(G@wr%e{qhkM&0+UmadKxwNKk+{3D- z+W!RA`ofV_W(v=c3;UFqLb~#VeQBI$6T70QF*y2(6sM3-=SnoSX$8;Bk~MCZzFVN5 zdri)xR{=KE#&U|Zbke=HXQh}u>;z2O%+6*z4i z@`1rUO3?X$@j^rInn!==B$bf7F~OZPLs4#u(X_Mdx@zv!rIz`8nOWlcgC?;YmvYdF zzi>##-8T9uI}WVY3;b?I9#+bdooq$a4BS!)bsWa~Mmh<#E(|-MThZ-gBP~?Qdzc}Up`tF8tr|sRSo#m{FN}hTJmxeVxq-q>d(zL^~ z^6PUk9!<^A{m`2~dF12V<6S>`${9^#FmDxp?7% z6bt5qNLkewR52r&Gt||~mC5`z{L)3*r*j_)I`k2>Uzt7rlnS1_stf=jb{Djoij0aJ z+RE2v!#Y9(T(CA`@i(7Seb8|nvMjd1Po%Z(oX*O>4CxKI2=v4{5vrJ4vzL%n%~Q8w zsRUIL>6R1~WhsQ*d#tRZG3%4b?|HYcykhdv*}YgeBb_B%)bi0w`Wnr^;E>eLePlKoYEH)s$4rShy`&* zoGOMtjikJ8HK@p}2TCGkHKCWY8?9&eM){ z$BL|;x2>P*Fz3^JB$b$%7+xB85eD5yQavZN8;IsiV<;LZswZE(K9FOnCt-%b8_DdN zixv-(11392VokHopP_w$aX?S&^SyYt`4FaOxb3;aBE;KJVmcKo{V1dL5!CEfEhgsi z_Jy?Jq+xh+AtU3nFT_*Dk>O$eKMJQ7PT$Y`K04B(S0$nvV>nzD!yVE46x}Hu6%uF( zDSXsuV{OaG8uxWAcRc?@49VCR!T{|0eozOd$LVL?Z=5;`WqCX*)GijYCNGRU1K{V< z2~XNSlS^Uv;4asdo`l3yNcn*6 zGpS`3%<;t@?}z}zyx%b_(yo$rDLL~cF&T9j>J_{CqGqyVPgwdbJG$n!I0u;Gj2y4{ z^*QDoOd{W+^o~&Vc=mWcjO`t7|{t8U~5liKaRk0p7bWrdcdG z)Zag#Z8iUH>f#8ikw-~rS#LR$RaQ=d`wsZC>CA9U=X&Sl`lPRJvweX|!7NTy(3*57 z%$&QB>mLV@c3$67B5OEVK$8&%A;rr=&%%69^Qvd$g5H&fRfC(n9BeN$g&nLSR&uT3 z1t|hUQkz_{%VVrhi~-x&*)&$~P%J1PY>w3e-tJmHb~o=Y%q`5F)_7xvHn<9)atsalhX!nMtULyxba|{TZIeDkTbGf%ZRH(=;VDk>OpW(OBR~$M9 zoXvh`ku{tB%`tYJw8H=1v#g}c6MmB7;rI-mE(0=vJnlS8fBSNwvZ~U7grItQdS1(j zIqiLxMisfGxi%MLT2-BTVq5v?Oh;{H;m=R;gk$vd4DHh%lL_4se2o#3#n%Q!pQ=Rk zGI4X~4Q5V^bTc2mP%c9ru5aoN6C@nJL^u@Kb<NgS^s;n$m=vvHT<9f>8Nd92E9fFAe$S@R} zio|5@+P77m>bbYjC_4W^9ae7Dca+R9)rsm@d%1kq}IRQV-)aX><_%vQ;AOa`=P@9|(UL^AgB&?bxSY z`A5CdH%+MT=;>a>0}@9l~ohWS-@y{EyEKX-=+lC&CAzQ z^3=-~rQeH$ME}%YW=BjORU!kb+s*F-vGAGJ@z`pE&(&CSduf+5Ld(%X8AV`p(qxioW9$Afd7W>de8FdT{caMn#GB9`R(wWNv&A-4^v9 zoN;8HmGJmrx%GypkJ$Cm>fgK(haX{{2zeb3TE2lnZ4gHb4AtsismEfJ*&M9f-^tH~ z+7J~<>NzAkd=QD8%i!J!Ck|hCWWT2Jc^{)g)mf>z{J;v5n>TMj$Q3ZQG`{IF4lc{n zIvWn0cqkX$aga=AkfhRvK(sY+5nTOYPzcwOe@6(czT;N z&qxaei&`Ha{qWO`F6#XP1bPdYk}_5{w#b@eIY;r-iqn|_H;u<-plF#a z4dt4&M_Hy>X*~{hYGp;gOJ`rhT(2@SE~RUpc?lW-c(8IZ?pwmOdCyYRcfbH3Gu=k2 zW$t>PREO(E;2B@x;Hw6A2&dAxGqCwQ*3-{(2ZaG}52y>To?3o-c{(~l_?Kwj_~J#Z zv>QHW3iK)5^cHKXj^)~wzrl;E;w?duNfzDW{Kc#eh- zvyS{KXHs%PxlC!*DH$l2T1=?skF5S@E9iVtod1d)U~8OdqWz}U$BSN8N5gio;j3~~ z0h!&h%kpbQnnB{n60%-cXZKvZws;~!e-8i6fmLsmutDS$_bRTPT%W91DnKu$g~{G# zdElOhz5z)u&OYB3i#aIBO%wP$H($XhVBKq1D^v-i&kYpqlMhU3Y4@zGSVh> zX>JrZQ@u+dl zL#XwOr@!x{Ex#f138{bz-RWnqPG8X>UgvtAVf>?l=#Q~=SPOikyY$ZbMao^=w*V!b zmxoyO+kurz=%l&S>4s7p?=Q1RKc}NpY4npVD(BEP^HyuDfVS(somxE+s()owNt9wh z)BdClC>hHXR*{@LOu1?sMN`NWR;E8Vlx`eC-9fNX8tKwsgTInoupn?BF`+kR+8P2w z@5pM*J6+(~a5y~MF1=;7lejkyb&uN zzMUL8r%%s0bQH=ZGrm-&xNm#j)mB&{!3<$q~7Cl>hf}*aX;Big*7$cHB%ybh;FcGFucW4>x zmr^F44^T>vF0YIV**AXhE30(#$ZbsK`9A)e9p^zCP!21ZLl8!Em}ieZJ^6&w=ZI_Oo8T1XCZ&C#0qulIcS^!uKU*I?~YUV zqe@}pE^JX3t{JWf6 z`|PrN>0`%@EhVEW%j(Li>Z%qeSvC^}lFWB2G;K@pj52yQ9K6~)tsA)@a9ldFJR(`)AN+PD>XN)?oRS$D{nBtJ3+#u+1CH|$u1LB5L z+o2ny!)dIOc`QDx90Ymi6SYzQ?ulwyk!VBJ>5N!vprS|)UfvhUg6DPsdmjyw#UeC+ zUXu+b%{$vbtMjb>(~tzz%#08bjfsQ0h>8X%U>z8UE}H|kC6uXTIQhX)NG z3152trn`3_{0A2=E9=>QjQHSpgPgaACU5_uExnF%p86*SuhbcYV{QA~XW_+> zkpVV)xt*qzVxCgrs(Y*z^D0#8;sNCfBUz19Scjp`s|!hh-S9Ok?l|3sm!@sIQ&jPJ z!Q*AGMu3S2<~zO-8eW(KVvEsFgRd|rAf8Q=sV~fqB26r9?$tRaX25GNZP=H}O4`ZTR*12^uQHJ~!OeEjp}~#5>0ah7iF}SG(XeSld7u@UBxBG= z5N~ZlPZJuMgRD9$Z=6g!@eJ|^*hcDPXymCI+1>RVKi4tW zO7YbN50ycBkukrSNC^}~Qp1`G2Y+SU*;JBu#i<2ttzrXwiW7VltSP7=p}Cc+%ogRV zqfE*3_}Zc){veNVs~vH2ARvqwD=lJDz^4a#mds7x)RHNruVc-g-#@RF~Eq|1Bv*1 zn6AQ2NuORp;Huu@&qpS!4U+2S8H<4)Y+S2+OBYgANp3l1!TGxg6djBb#kFwb^DFJh z{6%NsCKoL+FQ|gE{KtmLhCSBvJS#3nHp^WW?<+*h;=wHIrs0^!B%Q5P1u!X5ZE={7 zkaBBRW{T1VO#7;WQTMV@2Me2whxTeVL-^^Oj(j|tKSZBV+;Z_v=D9^CMz0NBoW)&% zwdAqTo8<$c4}}QtOMifyjHLoS`!LcDMf)WF0!?_H2C|c$m1{iF@6?1JINaWakX&oS%&UTxnD_Zll;&_=(5ye1J-kFURdA+X>UM zhXL%Nbz}9gF2cdeF~`aZw8CTCnX_}p?GkLOv|wQnS!1h|vJyt&NmMnpAC28t5lg%H zCHtE}7x!vLvYNMoRiw+sBxB#!5%dhJQ}A($IS??V5-B1CePgd`sE?TV3=#4dtY|!V zpDS1GM&qAsmgNeX{K4B;iwFSglsSQcUQ7CscJC8t#Q&`qnTtfDa?m(jZ*i^e*X6v& zo5`0&DV$(y_so(mJ^1|R9cmn+_ z{2pA+@k^y37Zd6!2&?t_H5I@oFeE1sIxO&@k`4*^c`w)t-!rs0YW5pS4Q7mvFpP9H zkfkrGnbv*34&EhYN3Dg7Zo4f1Nr)HrAIs}o0=_XfoR$-Tr(Vx}d_Bmw$jG4IJR;pX zKCZUtfB?pfDo!Dz+TUX}s*BzV3Z^Va4)TE~%u}rb&d(o6I4YsD8OZ0zAn-J__+Z2- zA`<)NT9{gEt0;B>hwwO~H5hrxrx)iP{B!o48?0@j18#y7>|W^`&r6JsjTNMx?FfeA zD|-VILc`M;ReRqb(3(OHFKx01sfvnZKFAzZRaK{=(%0P z6zv%vhwBl`WRB+@YBFD~>x_ZHm&-J5!Qe&Mqi2ma&KybBPF~q?T ztJ-hIoRXLb<>ib0qAFikvibP&RPpoWirt%DFO-qD3%-fjdXL~Olf?LJU7T{csI!v2 z9aErzS7_g~`@JS%7nPGJFipGOGJ7P^ z8`o5RFm#B_E$=}>f`?<|?P)5;>l~4;k}2D3#bS*{ixEMH6ItEtG}IUoco{DP7R=oder8upSyKzY zdJw2?247Q2Ze=u3)@pHT4SvmX9beWrk=wlgKEd%3!#s2-1q?5O+_z%x?(dKGV*cxc z1cfjdz+3~?gC-L~Is>Kd`QM>)84=J=cJv1<#r*yzYCp*yFaA% zpU=08Eo6Vmq~jb3PnJsbso9ynKx=Y=>N_OAPUm+ae(*5JJM~F1BcrsOT*llJvwdJT z0+X&!n!v+GMU&=3XuO0`6*Df?`3quD^b@lhxc&P_msHNsh_O+(SCbAbKmcGbO( zGFOCK)54h>o?estCb_}+Oipxawrz+7WeTszr!A3Z(MBXtBf zQQM@QUOh1B7x461Jwei;sA)uKzH0ww){BS4#6`y>B{ed#Y;Q`Lr$ngc>!f>7CzI_r-q&EgvprFIS9Iz$FJ8mJOkA`@IijXl=Zrgj(vkeEVMtoZV#6+KC|Fw$sQs*C)cv=K6nrA zj|5Y6p>e0+Y_yO{mwZmcL`P}=*NVSkHP^#?LfV}sT5G+(?~08;w;S}diT-g~xhTDy zKL;Z5Q19K#V>p}uuSf8<6UJyE7zrc=yBH!D8CA;hQjx-kJG#4JkudqpKY(Z))Mgv{ zlth4VRnhJ0I>_$Ie_j`?8S_d8w=qjO(B`4GzSs$RH=AW? zXz?x{KAvkOSJ)S+gI`)EpwZe5=WiStTFAy^*Iw*b={8qL_h2zJ6^N1J=#ka4?%cF< z3$bwtd9rhW0o={vP1t&w*uR|xaAppMs^1qT-|Ov{6{bc~AKVkFGryou{C`NjyBStK zeDzUe(;t|94;(MZ`P_KVpmwO{>Q%iz?C>^0(W>+;{B>|kT-anSom4ZmW^Bu03tgScu*fL~18Ga~=xIgFT=(Z8<= zV_{*Uc-kht6)4FM)Ic?+!&2_X#T479F>Wsl zb3cjgHCq0(vefo7l=Oq@6;qsDdxvyNY1`R2?4MabSo@?CJ?F>MD=RRpSvVEl^n0SU zW1V;#%U9n%Kt87Pcb*;nvG4v?6l+!+Fo|sDy)Pb0jUG9v4hNMLExj+O!9wjf@F)Cn z^p~@o$$$3li`PV&^3F~@N>b#rAuKB;OVKUuz1g|b;)s?FV%Hzki5pY(x#{g`PIG@t zq|`r#4U|7!$bQkoLzri05Afd(nfNP1vvy&HI0u=a8rV{iv3A5sRcm&vqOPZjMr8jm z_T3NHmdQI%&Y`@#eTMKauSc<){Z!~EG9MD)<6B_f!T8Js5;2Ru0q=^2M!P=Ksz?m} zdQ>rFS$PB{Fj{zuY@i)$dOgi1FBYm0$@UoGdy4VVH2HNgvL?Zlw|VsSKW~w%#GxxZ zL9~d#>eABpXq#HIZhroC^Ks}_AgxT%$AzsWW=YFR=N_wZ`!8Nn#4Np)EDx(+#NxWo zq>LK%izd3_f6niCv`pj+v9h$hlJ$T6tLX^MYV)`&0hhLTkU9(Y4@6_MV^7o zi%B9@NYV?bj4SFJfMyl1>M@N9gnDPggF0$_b-h?w}B92>>E*(!>qG+Jg)>^N^CJRhp7(n zc+ArMDoplXFT)>LdQ{3N#m}L3@8E6JRIn@7XZm*h85%j}S!K}ddK^$AI^OLbjYVLr z)ns?kLwza_MUPt$_e-Bo8zqvY_m`6(b2*~=Xt@1cfwxen8~WP}fbdehL)_A1PGqd`ePc0b5Gtp`bc4p&)iTO(zjl}VfZlXaJt zzdD{8LI8qi;&20)jwM~wT}ja*Nq4a1m@dvwr$*eMhGE);KepPkNM;MfHQvvr88QC5 z)<55p7r(9p(F9of$6@{2)|&lyL7?~hFO2!0`?34%oqSa#^P;zFFkMbiL}0kzFOAi= zA??S`^fM*!Ks8R#%Tzyk;n^Lb*&Suh0^#`F)yco8mGn%B|GmpakNyCukQ7Yy%>c<` zV)LMJ^Tm1t_b7mDALL$}kW zVQyXb#nI~9^7t9qgUolSw~~2|r9e!#K(>EL+)cx7MA;+F$VDDHM8MnlUdiAA{!r^! zlcxGkc$D<6MSro+3%6Vuq50LLzJxiHyIaazhsb2oA^tLj=k-Y`ng4*Od4JHYG7yre zhR2-njng(??2&mn=q{S1yKyA)PEe8~_GbM}7Dr0=x4ot`b#lHI-NE=+5y~H5>)iJ3 zBU_Vunj%%_x5+IAVv-Cf0b)(HyrAw!nA{M%;I@&<6;ZYS%lP!4j!;#_$nW1KwGs^N z4U`Tf9ka~F9NI2h60>tVG#01^9H3os>mE|#1NH8%YYmToYzB^rVX;0pWn!@Vk%grv z3d%buZ^IjF!)U=GBGzqoy==@pYa<66XC7ry$Vp^a(&8m!DSC5xr|lix)m=SCVS;LQ-I_va(x=Yx)YJ?<|J3|J^tpwwF;-Y1>&^9U-Q!Nu{&TTV)+P_X z6T#V8=MuI>sId8WIZM&ETi*ipD_Yf)9=#!Rz%1$A+CESn30#md)bCx>ht`rn_7x-6 z_3Jw2IlZs$SS{?eKh2@3+-1nzdumwjrcD=IRd(0$Hd+AE@tKG$PYnNU$k&# zA&7YSNXol1zAITF25jAsEsKlH9-M8%P7j|_Xs3H0vTuq~;dj zf$MQKDI3_+61x^^QD-aqLSB0_r^Rsnld?9y9kU$$X0}&C6Z|n_@1Bj5c{`jTyH3y7 z`o+M7{KNeCef8ryH4ia97l}{eKUNqdp&(oL*NvYOHgoNdtNo+)r_VFpZ-&>cO?$+M zuPY(ci+#?gI&_n$d1?Ct!pOls{=_Gbp3^LZguuP^hslA=g_~CTb_$6;tcQwV5pz^B zf-a7`0sh|e3U|ud9;93O$<0}_>!8C}fe8fBq!!X|Vgr5K-tP9Y%<3-$tv_KV*EY{L za;1Co2m@~~NEk(pKXlbzt6${{Zd35W;o_`XaQx%iCXJ2lqxJhs$xYWT@_}62Sks%i zsKiRi-=0B3B3l_qh?7{|(C&u}>r^UFDdUj#8BRjVoQGd#4>*C$ot9WMSzfi8I@saQ zCNjTRhd?u0VR4LdKMKKC0FcTy`35^|h8`d5tY=(@(V0Ya2^oo74HOWPup|2Lyoy=d z>e)C6d$eIcak{tds2(Szn}+OnfEMGn%2t*!8=@!!=w#_~_zf0K+^@^B zV~P5X~*Tg2GD6>RZ4^}@G^O>RSpk-2LtuN#R3`B>YN|PE~pcz4W?W(G(d%9%z z&>uH#&pE)jv~ssC5eZwkYR8QnRxC5fH+#u*`60)Mz$f|DQq?tmFn zR>D1I%j=(1`SMy!nb^X4ki^OXliwZI*>{ zbPd$(&^(A6zoPJ0Q5)d8lnN?mNJ{@%k%$LXj5uVv?8Y)}uKb~Tr^RgVYD9At@|@dBunMaM0M4tqsH(O4>*W%JV5PO+ELdHZWGe@h-jKADWCQ`s;uTvB8$JIet2qLs~eVfzwG#DpeHR_DF0X)_nIawoE>yDa(cpr@L?_MQdbG`^3 zR#fAYSVpa-O>R%YMl2NDM!4L9hCWRJMap`AoHXIIjpZ;i&5g0zS$2-#x;@wOXL8Xk zj9amvb^K1yJDjie4|Zph7@J>qn2aNQv%)M`Q|FYNP-ragM`&s4s`pW7E`jh!GNBqY zTl4FpEVcuo6GEV(=(eQTb{>h@`z~{lS;a8yn-1e{ubxT;f>|i zrG$ntISog8jA_bs(J{aG8Hv z<;@V`8&n@ls@L@~w*{PjQq_$kEI&}6;e}f*x|nV6@oqenS2pAsJqfM;ZIvb9`e!f7e)c6fdSV^Lr{gWm z*iJ=QOfx%|{74{+ENpNb|FQaWEQKAi?tXeC-p~Z{BUsn9%Ws}H^wl8 zsi>gS2ftVoz^TlsIME7nJ3q^{d~(0a(FXX+O}EXRVnyC8eZ00|wr%1ky^Gnn z-YbrGu#IIsM@pCWHu;C~#R9_i@^c*QFT1yS)KkxA**eaaEA6^mTQ?JeY-Jn=&yIEN z2$r9##AAE-5p0VLRXv~7xhkp==j9htA z8MU|MZ}d(I^0jB63pCjG?X<1{GvEd5Hf?KZ8QuVdwX~y~XhqfxtJTDEMLwnXY&olt zUA;z2&$iFoQJ&*nQ zRzZwXGG9Y~fNZ?QODw+HVMO)~?Jl?pU%^gMr{{;e46;bKCMO_HP)GAl!h(*gT(fnP z+wzO^38;>t8Uv!uB>CwT;kp8usM=&RHpj{Z)&BA>z6P6+`0H8nanVH3R1k3r?|^YW z>$CCg$lXu{88ASvuhAKee|Rj{KZVJjT4M0Y_9A(|Lr%|}?ZcimDH8FOg|K|?hl$ER z!4`vbyaE&j6|1VkFIZE0f8zSygvG%>!Q39d# z0HYG13tc-GTy)O*j~{k1kblnc!PGd~sVDz2l4HYi7RG973i}M%>)MM2!J`je77o`nX((=vQX62Oyc)ReJ=ug4i4;PL0&y&q+ z>Rr#Vcx^nqo=|`?kBkoXD($-Inf0OG2dk0jv{Vv0-?)b}@3i-EYI97OAB?(|BEJ?w zT4ZgIhBmUk;GJBeI>w3E<{1;tp)T533i|bZT?Y0QxeUXH415{rwm5d7hoG2E7A%6YUn7 zy@$EkXf4Zr<9bkRLK44mR=qL+k7h_KPms~z_8bj2l`!tz-AW?!9_9gA(tVOls98rxv2p?df zB}sF)is*4tAxscOYE)^lbh07pKqQFcu$3SPsbGUxMDwwzp?d(q4q30edaB^mZ@q3F z0Z^FPY*5!i(^q$K>2{K+cf1FZerA1k_Z*K*d+cb`GemjCd8%l3yf(un!*(L} zMT3xm3B2QGY-Y*n`!al2K6NXlmv=7vTU^tk=~T&+KyuzB?Gq&KB^2nmUp+$9pe%D+ zmLKrF4EEYV>37w8GYn9v0*#crJZFer=@}5+)phN5I3p1l#N8f7A1BDN*6?Rp8Qu&y ztaj_*BVX&jR@ctOB?o3XAJl;sbWVi-C19KJtHL3iY&J=I?rYE(}b&zWguGZ~lAQ!;~$<{o_BAr_#8Le@&IL zf3W`dfjS-_ewja4_23S=~WaTMqv%WMOy| z6zZy$*u55=^qwF1TG`sb8~jx!9vB-wl#Wjb(!H-5nmva9{~C>%q{fDgMaYhEHG5-V z&^`nUiSyt6xtK^+@ci-E;A7&|=IH|P8dq_JUt7yeM>CalNO{xmRO=dtmA5{#ddUd2 zy72zY;3PbdUY2xpzk4S;iBe|>qw@VnpE2<3XZkavji2MW8{F}Pgmmm2oI(V>p0&7s z>H5CovEa>Wm1yH^6K1<-d$oauA>_Qq&Egfx2mR8hGk)VybXpDdnBbV(u!s<;iGm{W z^zTT~E*t`HB_y-hrdt_1`(Lu&`;#(Spm3fOH}Y~@^;EQe%JT*>hcx57q=P0J58Gtg z@=Iv8JeNIfc5bSLSx=6%*11r)`AcbH*uM${yN@*MiEEaUFBEJWY!tf{nXw>_*`O(H zlQwC#6u@&1^bgy%P@0QH>lsU#v>_YY@?1&B2yS6X;Bo0*yOL_dl#@Zskv9!-s5PUj zW4CLlPfH`>+=VwG`#0I2H(SRCCmpAa2E?RyyYgN=DM+!^DvE1stpy|L+@y)RNoxI;_0o(3P2>8{oKJ$wT+j*f<$m^fThwJVoB|l^L_% zEjDB}ydJhm+Q8>=ytcg9ZspEVOI9GI#_@O1uSvtwI>sQ2V7iQ^t&+H=^{95Ww5|H+NL z4VBwZOjp0Bqt6dt2jShGA@OG3zMk0qMgsjs`Fm_SKJ@g|QF;VD2>vb>f<8m~O!7Z| z^e1rBGmxrXIxfa%`Ty}P{~x6PAMvQEGlrL|ql!X)j|E45zKnCx`Nh~rLjX57&yi3Tui0*Otxp-Sh3V-}xsWd0mFm#muU2Rb zhTOyG&NNg=mEhiMf(SLbJ31KPNjP^yY-;AXHK$4p4JCqFqU<%U3eops{O4YKx~w6g z%bOAR|BJo%j*2P>`UOW(F#raVWDq4t5|B6uC?Xjo=O8)9VaUM%2nYyBPD&cdd4>!U zCFh)T9CA+c8uk0$*|&T4y|-`A**$yq{)67Rx4XK!>Q~j()pZFdc0Q{(Wl?z^ne;vH zp|P!vC#=x}Q!sYLTRPxZet`A#`_@ADi?hBzk1iV|Dh;?-o5s?WU&jk8nvLSqgiO!L zoaq!G#`gHM46F;w{b==Skam)M*vBza)CDtgp8;y2ums5VDv5s@P6p zntsO1l*|P2zAGDmr)L8A+H92vXZ630_&8PX%RU${#84c%p9h5N`c& zP~ioypwgBhaLx?6o?F|@S1ZH28XiF)Hd5euMqpq=vY!H;3Q;leWPxkHN$=jy}pO}-_EJTa52aqEC4aH;*G`&S4` zCBNnG4zD+%C8!rwN~M&MR!G|(qXeG%+Q8URsqk8rrD2H{@&Ng>d611n)jBY{P@-Wy1T9uUa=FUZ7=->a|DP zDE#YQ4yYHp?LR0Ae(WntzppN*tXV=m@8JK+&;ln@2x_N%;8gCU2yjD~^_1XLsboAS zk~sL`8}88chy_VlG;fk?PRY$-EFH1_M6{F=pCR&hfqs&^dLK zM}Wr_OfOefQY`OeU$_8#R_VE`&Osn#SKRGTSj@6Jd!jQ;15@|4%V=^CyA|H=f}*I@ zSAW>?`y4Zr9Q3k;HqzgszObg_MZ#r`7yA(e^oLMdjHr($p_hkX7pKgWU^o0H=MMeI z=GAs$1pY~vtX7rdROn9ysl%MC+s9h7x$Hl!q0Wu10m1rV5&QSq)q*Vz7M3!@!-h&-PP+f})_k)Y56Z z(K5QUF_lE&O{bPu@8y(6cdc{A#ETywODIP3FKk|;_m!Gvqjo}+N(82LpI!uL^z~}@ zIQWJplzymLz2k{dsDGwkAf;dOwj(M*R_sxaW`jblz|j4+@q#7!kiwKsO)7@AkYcC1 z0z+uTzsIrCvzyW=)#?ZT$CYn0Tr>(?nHYr*(?r(2|28Q0P6pqdeSv#I}t zI99Yq(4Id-&e9et1<%oWU<|G^K>{QrWrRM4Ec^#V82$9=ef}qN9!?{{`|(kS%qIp?6=Lk@3iNfl6#7J zik1w)Rx>0z-pY5kDp9gX+{~=#<_#+ApP2MyhKY%7EGs`e6ed?F&3|l@J4boIzNmQx zmhE*w!p>mcb8u=v@8%k2k*Ko(k0NFjj7XyvwH=|&yRbDGbIZvhT{SZK6+N1H9lBa+ zeBL*wN{>EnrWZu;OL-0)FXdQ8X_hM-Z=VQ{EXIoZBvh!du?KD_sXWhfKaVf>VwoCt3~l8Du#g;P?|Z5Xu<^4nHB6S?UWwJw!F`QY~$MHhNA=e_F%tm1F+% zH00b3vtN=FNc##7PIP-q44ZJME4Qt!Rlgf77;Zc}qT!R|N zlaex0fuZ!2UfFdy0ki1H!{nabWZqu8VzY_Zg@LvvE4$`2y#m4z539Z=+z>Ah9~bDe zXVlOhccnGmQYl7uive|oImhecc=Cs9v_daD_f6Ith=p!*Q5u%oCgDx0Bqm09IVI= zBlmq06H^(+H|#~_+ke%gYdF7TtBa*9HFB+J<$-AN{H{tsjebI1=K-x)lqwt3dQz)G zetST3H4?p9Q&Y)rI+=JhZ78)?;WrlYyOWE`$DP$KhzzI-lA@A4I9it)n5;=%JWg)7?Z5axkQ6%6Dw~ z!Cgy?l`sBa8KUk!qHpGqZQu2&$E4&yj!Nl(9g}la#H`Ju5l!BoW@g$$grYHn3i-k! z9utc)WRa@a9b--!bu2kG!HF;Ep+jc_c_F56SiQ(>p$${F8!wE*bpOHCyoi57{`+(S@yHi;w*?_Q2K0OriX3G@DEw6qimy)Pm0wD_TLxQZ9Bf? zn$kmKu7vZttcuvZ*!ya;+k^`gprLGgj;hN3cqd}9>QtkCvsD&&or};@7(KZ)Mq5%U zP`{%u3#COyBOJw4Y{U0)CEhxJ&IZsyH+&lXfQro9?D`F*Q!uDrXp}q zLWNl-luzwji5zr_JKSh967pn zXO`)jFn-OPOp;V;g_Buwah*#?wH6&MG3tLxgpv|-&xb~EjWuL1iYGB-_A6`;8rPF~ zk^HKfP6=^^SMRKLd2P3)J5G&M+SP|WN%A^QBgVeLaVZ*4f zB6HY2Cj-=r9K<^IO;0ZgNZvyfBatHhhI#J{8>fYAFrG6Y^I9UQYYyNB9)CjcZIlKt zM?{)lT4ABjY%3Vi$7%fMv_to)v`zJt~VktKLQ*F2J)tBvIyQsoGOe#QB{0R~iyEb%-fkmMnhohY(J7C@bCVAXO= zY~+Q=5>*Z9N$bw4QF91J_0b5^4ds$bp#RC|zrBE+X_saH$sjqS)w90qa`hr@HdL*d zJR46?VVJWq%@&m)us<{9VY8MVL`I1@3E1T33Rpt{r1bS-X)+S4rw6h%o@S-1&bSIJ zck+-KPUNOeRYP`#;z+O>U064*|Bq5`t+YtW(=jpQCsuY9pHfCUU6UAI2}YUXcdUI_ zZ4T)r@-UTt3D9)5`)E)RcM<(p8~$h`2o(SslBT1Adah>u$os=HU!vTdCIHF`~6-2G`K0@8(4439POXoqjv z=u$D@JLY)3)3{gx|}BW>1Nc{0PXfATJX<3zI)xR0iG z(Fvo&TjnR}NA8L4V@i=9jFL^d&Zirr4%R17W^=waF9z}}`HwF3ZT4gM)aixL?_l4! z#JemUft`szl8H06%Yi2-WAP?;OFqYI#!{5UmH2W5r&41dKD@LVfAn>BfNF!l@1r{A zp!=Pj)@KJ1I0G;tnwFgLUS|!A+C!r9x`((9sp#oblk(1hx~1J~SCuoA%F`Bj%0nr5 z*hybtZ1Ofw&kMpQChU5=zN5QCgHz^Q(@P1m30}BuV6#;hpNy?dn%zS5trQk=jQBrG zmt>jP-&?}6_H!A#-$qcS#O%0Bzo;s8rx|!taUy}nOtw8hHZ*Dk9|ZtG(!hL(`=~u? zdA(lxTLcS%SK|zI@J+k7pXXk|fxGW5moHb42OK>+8FHR4>osTdC3dtdIoJ&g`%0_6 zKg`V7OHWIN=sRT|yER7CIPz0iJC!jgFj|#M{nDrM=r7uhQ1!Y9)Xg>j8J=JyI7+V3 zua>iDqiiDKW50$P&kL!%zry6Y9Xzb(@m-o=OjYG#fji~t6E4cA;bPk;lBcf39lw22 zMkBlRdj^3qpqt2;y%fytG{Fi(%iVGjc=>O((K2Aju*#NVtNkuk@hp1^HAgprZ}?Bu5rCW}`X1t(lW@ z8>P~Lx?h~yCVxjblC&_nkDAW%OtJH z$CMkQ6-9v$7C#NF7IUV1?#%F>wG3E=GNLcEx7Yu5UV565*F6X-UUv1cgB5h^tvx&G z8yW;ON$Nlh{2u}t$6T(FE-$_7|J|k3e?AtS_=83pe(m_^W-Y}w=L{`q*&no{=UOFlowSHWl; zE~yxO^X5&lk_P-XUqfKvP*lRzXfj?VLj<7akQ|JW;fv%-#Wh7|k;!6xWpWv|x6+P!Ipp=89e zaIU>ji#2L>D6A!GmdgmpBzI@P*h4_rIk2*|kDlE9R&HogE4@-4+JY01T)cNBu7eKR z$^3i4eq~SCX2&Z}n=CBhda0MKinKYP?!XQ3CeVGbM6X~SCtBvN;Ie{NK>dtu8NrBp zx|OMTmvPa_S5Nfl*LCJvkKm$5n{@?!CGeS(WY*n+k@+2<#5GPh!yN+?>XB*oYZRg!Z zj|y>Nnd_=K3Rh%)B9W2?;TmZJ$ev`0l(V!(28llXIDH>F-#6P2!fpp`M>1I5KEpB{ zQ%b2A6}rZJU|ct8$aW=?lGFH!@VL5KXNXT1eRzvQr(jpnltY*cP$m3`*TDUQ2or_< zjX}P8@P}vW;(sHQcO+n!yfmco{}Y5{e*8Bk>GSwch_dkO|EFK3qNrua%3&c2@QIC$ z)i1a=!J%JO<$;BT?2hv9Yqg%x^m_47(Gr5>F^L=B4Mg=Kz6_@KvZe$-=Pc&1V~$>4L%RQ2SqU%!@# z9)`ygCUuNlg_s17mu@#M-v|~9dn_XpZb8@o+Ru6c)&ylcaG20{W@3I)!v)8N32VIh42*?FeMjP3+)E1Ps9Pn`EBO-)K z`-;KGqDSRdA-oGf=Dw(vfsCYuWG4O8yNQAzWcdZRGXQM(On(c1jr~j?7k9ts79~}a zH|JTiy|0iU#8&#UMMX``_67SM!qDDkIo(&V>+!4cpms83+Vszl3`EKJvo5!uF$>|d zU4igwJfNuk-QV92qL8}}^wd1*bgn{diy!lQhcXh~0U^xFYY!un{G%Wca+d!oUHC1A z4WYH&u3HPhU07YJxmew;dZFDjFjBVT8`ss2jScxudb|wLyGF!PW^r;%tR&>6I!0D8 zHuWX9Q@o(McwS5>?~BlQQH53W;4<*2WLb`%F3BVvOx1Uel&zgVbz+K=3R>v>90@_B ziAqJLC&9Xr9&NEkD6u85uHbJ|<;>7xojB{r^E!yHqDTnI)zMt=s#>m7kJ9Bv<>lEd zbg78dTzM$PZR5U!Mr-2La9TC3$K>qx{u0xsh~p7(X*iqQ$X%X)N3bU4nAQ}6jk~Z6 zf*7(6$L4hVhliW!W%6=n>7R@Qw^zQi4G3+u#8+P|o4oIviVTvYuruiiOXJjBO8Ypu z(TN+9{Tc5X#9L9dI#%NR91D;Fg}LhnM#4^=K6qcvHhE=*hmD&Rde$4<3VJ{8fE!X{atv{beO(}*@yud(;So)GbN!IOFZR) zX&-d)%hPb!v;q?v(MEd_o5?JPcyV^#D8fU}m3BJLvlP2_H|bbjwRtU15VM}?=L^Sz zc)weaYf6to>bQ@jIo$TC>rOaU^>|m`L=4|>_3Br>|1)Ey(&tXDtNuh^4~>#h(>PP) z3*`3lDc~ZDZjD(-6{-Y1>^dv;)SRrU*JGH774spX&~&oiy+JmJ7;G6)X{I?}M;$V1k*r*($Z@igtVWrn=In?~ zb!r#wg|S-d*6nNC3!kJa55EX(rA~;nE;&m-URi3?&C48*_q@0s7|9&uS>kR!^C4?c zsAxkqOEpdGHS7+gmVF*@(d%TFY~@gj%_dU|S%TTmr5e{^AfS`G$m{IP?a>x(TI@Uq z$yej!t}WxOMovynu=~no4hy5@3I_bn=!m63=87u&BA{0tWQp#2=jjdzSveulgGjRT&(Z6M7rD$E;q_T!#b$XLX16)T@ijB}ihZjLA5O^zau_hs0~ zt0f%TvbC+KsO~iS?qE%+4Ug@%@Vy;D7MhQK8R^N3lyh`COr2Ue{_2Uen%ZtCN5Cr& znwNi?V%{NiyvHh6D?=33^H_v}9aG?s#z-`|i-W?=J}1yUv+^#8Ad>vuHXFP0_Enrr zudGHSht#va2jvyraKEqbkoi_bKk0EP)zWjdTQNvC%P?|7CF;-73OHf49knB zb;NOB6wYkP-91007pzu)KE-QiuRBriM&BC<|Z}w<_oquC`|*l#sY- zDgNF@s2EdSrHdc+@n@qbP3|2#Gv3TB!zJKkm%d8+B7y4*W@;E|ZuH(NZA2fb(cVJl zLtv|aoRI_A<$!Q`qAt8s`&(9C4Wg>}g9F4mc{OY#l zWHBg=)hv4d=Cybr7=pdeczHn_bP?g);aZ}0=r1SaFv@ArMOFcw3bGN;{-LMm%Ojb~ z`21nCfpgC>U|`DIhDAR4mn#qf~a zl5g-S6s!=$EdWVsio3__>y~+4m{z7dJgpN8UV%qCo@;S;9~%%Tw41gQc1=%zU#E^v zI!m7AJ7{N*oudrHNy;3NKb>|`jb_H5)ZMUK%0trIXg_Yi=6RDVe|b2{J8REBq0^FE zoyAz5+~UPX^${`J2o8tWiP3P$Rv#aDxYfYBCZK}b+Z|5*X9>=phvrg!yEXSWLC{G{ zdjqW!c!*U^JB3-qdBIjL+x+73jP>ZraYD%1vScEqQ?vgZF;p&aL7-~FMYIvKvu403 z@T$ElmMoVzs>uq)-*4=fwY$ZM%hIf2bSV%kvPr`$XBV|wTefgG(rTc!v(sUm{TMN$=(PcX|OB)oZiPg7Bc&bY?CZrm{C9Sf4BW1FbQ^>WK z^3JXmQ|6wG5+V;a(sRZJW~+E~+*UO!L_S_YD+c#4g9ZD-&)iX$uHcddi~dD2dRak- z)R$V+g$_V(+=Y|2NZ3j%h9akI+dAT;(2X;;!^WC?w!6lYjUh?Rwrmo4i)6F+AkS zoR>e`H$-u&+`<*{Esxma)yMZfI1;=CpEN25&SpRPwDNc!?8D#U*W|MAPaVveGu8sjI&RF*!RS8~@AwPDhU z&=IQH6e1<&c)Ve0F0WASS%+w=nlKMlRU<#GcJ2sDs-DYIl->8Nbuto{w+pyS@}Vc1 z$+K~RI`^iX{O7g50iqhapu*YF$1+x$CHB1J2d7w^d+ZMj>t;fagC$Lu$Um26-Ci}6XF8D2yY@Htm%d0)U zfsGxP2qG@G+x-!Fr+$Y67kysbdC$~$RL2@Qf7EZ1g@8%!z3SKS%WGDj{=S7RI^Vv(Gl^cVpV>U0>lE{R?t&bO#|0qwf#J2WJkE72ea{h- zq&Js>9}L!V&+rq#Hnu`T-_Uve>4tOJ#Vx01Xu+oFZfzBp80AFNR~ox}QfJlMp9kA@ ztyJx0l5gVPXh86I9mXAE38L2)C{b#oj;^CM<{76jN+Y{qP+~T057U00AJ^EWAZBN^czhdNPW72g)c~$SH|9K zns~bUDCa0aK<;EZ_ie{Q?!&+@Ey19AgTg$iQDaFVryixCt9H!#&A_^(F2|N&2($S( zVP@Glp+PA72>TU?aKQ4vSO7I4>|dDa>uvsjQ33JmmxI2%Qa{N4iw`Kgr2n^zpYa_; z3U={tA0*BHADk$_(V&C-WFLNl7FLkkH(S` zj>QzE9v@tR)PCc*&`o~?#ac9h!2m`NtiTFCir))*gK=%)6cl8I-ps#dtJ=PuJ1u#o zxBEuiPQ*SHW$+hWS1)OJrw7x0Q#I}#IBb?ApaqlXW{s! z=H}B=wxCa9hS(C<3uFq^3yelydZ+nST=mv6QmH39%$?9vG{i15_mYy6QWYkcRqZdF zOO^jOrX$fup_7vr$;ls>MIzUkC7wT^ygx=xMHNMHO)ayO3Gz6EhoEexd9$gyK(nx+ zsDh(Q-dJHujqtoW&dTY<&=cr7p*3b1jk{!s5O3b9)&CCW;0}kN;6+r;o<n#W)O$C@oLu0{y2D}v=cO|s}eWRa7FA4cqXzY<_fa{r>&i z*3{XQ;W6}1zJQ%7L_9{tshcc7jj(F8_~9Ps;gp`@!GvX&w#mR`@jfm^#l3N_Y$soN zW%!&yaB^#1{BV@MHlpnm7Pr)2OMv>MmS!C&lXLgXjv9V3Ea#?MsW!DSq8y5<;E;C%b!+6IFSy#JoHR`JIO3tZJe~I#&mx&x8Dz$2y+~A&<^(-?uZHDR~gDJlD&AmI8wx} zD{zGKtrYdvh=+1ihht)h_v%r7Pt#%L=+{_#i|{s{g?U|Olqq5cS2Uh=(+~3a zAAm56a4o3hZ0z`+&n&}Ublf-x-PtJbDSGB;n|Ro3ZqFaqWS)OMdhX!cjLMx<4kgz0 zta!4fT4KZr`^+Zn5hs?FB^6{W(v*!@g1MORzJTl|11SPmTSsm{rO236|0r32ds%^n zZ>OWCF5!FOFvXPd=)C-ptU$4qw|iWlDt;q{}m;iqDv zRlm{&N^)MGoJCO}URA-t2G2&SJ7&keBp2Vga2GD`0hv(13r%ay zTn3<$ep-Im7`Ts4ENkB#@h!TreSQ%1@F+S(ygiSb$&ycoHR)jgeN;tb-bB0^>Y>KO zuUkd_8Nu2&2Xl)uJ2Dme$PJ`zPuV)l&r=jVHN?G7kZq$s_*$jGJbbDe`le~X-yBX} z3;Sz8^juuq<@eg4t(oghF%>2IxX1N9Et?6fc`B*|4B^ny^OT&8>2D5zpT(orEdjKZ z;!hTn%H-UyG%&E24Q4Z7VqZ21e#c`n%5uX2{>Fy|QQohn9$i%H zArtkK=1QXVt|^82pvLOhi%|qnZ50&k`uIi9-7DhcRevm=T4v>s-&kx;a-KFkCW6Pu z55!_5tS(NZsE*r1i!SL8Kk`l4&avh;T+eOKddj6TY@Zz z(aluVh4Il_<|sT}eV*&ozy5sEZTcW&a?*BVIe3lSh;F~5T2rN@{NpXH*`C$pau(!D zwX4YetYo=WO5z8#RKeWUOF%W0 zbnU;z^q1E~hS$aJ}L`hwg8`X_ny0Mh#H~)7jh+cJ2KuISUyB6#eE|$pD~aaX%3W=1jzwnHW?LTDo^t z)TA||sbof~A3P8y59T4b-~7m2ET+Zs_m*FZa@w1mk45!Sg%np-ME-gTkqBIH3f7E$ z8K1*SIYS~#v-~Qs1`7uvvdac2!zVVn{lv86)D8akm*S;FwD^<048XF-NH3x7saSaV<#_&l{Hpc=(!}5*+k?`21F-+G zxhTr3xQ&dph_rw42l`0}U%+_v{fZ}TnacxFEFkwjhEYOORu6(`WAe=^;XYVFeo7$p zzV%PnMs|E3ISIO5Oee8Efsc-P*&%am#4R)kJ&fK3A1TLSU+_+^Z#{7YES|nAMmDu8 z%Udvb=U1vPcJHM-1};!hZ_;+oZPG8!P0(R}9aqQ`)v_RU^Z*!Y6SB?#WuOM1(l*@> z&Mpk9S|$yyV)dh3^yB_Y3smSUGFM(jiwO$$CwnhFb}>jkHQZs*_X9b`wjX_`;ylId zA*6O(J0-hR^fxpH+>RI5k<@JNWUxLbTfqQbZ)LU-|AJE?bhCz;5H+rHVQ{# zhutgpV7@2q&2nv5dij>lGwzXwLY_wMf+y5>3WXoyTro4;l8P)IqEKO|yV7!3Tj-*O zQ|w}~(`oZ;Gk)$4Uuk*D6#gT+-|FfZ|BbsViZ2Ocws~pX;@sa_W+!hk@ zKjbi06+!TM?3=0%_sOT!fAWIUMAUl!NU?j)Ur)-iDuhGmL`u5MA;Q=HF-Lf)w~)R|qx@A?+m~tQ zu4&?=gyC{{qhny1pW?Tt`%Id5{r7j`Q)go5O>Pf*Xq&Aar{s(n)agn7$2@cuUpp zY|EKLcJO$!RgUtmZ6+@zR6)uTLi|0%(dtDVTZYt7-WFx&5Cn4&&^bUY~xORjTtlQh6D znFsRMn1~uWFbohoFxd43MgOXP`1aEWAU8eTaAO|jrpvVLF+Z8?)KL)*0Hc#vcmM4c zYN+ko@>V9mmhf=3Pcat#ftQt)sk3*L!^}&=n>e6xixuj5~(AE*q&qFjYI z29&YEp5ESd-@l7z?>r#ijdg}Ut&w*GSQSN9sPuK`~N!>H*HaD7NgG&pY7LnM4rOv);LyixhCRf(Z~sS()~2a z2}|NL9J$z{oyFjmnB)PYCOUI7FDb|oX(Sx7Y&0_?>7zgs-ttZII-2>4g|)Sr1t_y? zUFJ~{11e#?!~;nddnWZN#4i-{=|0SPG?B5z>f@Jq9O^)}@PQia z;YAwp>EtMgN~S4{s+Y@UD18lF;zRVAp?F5JUx|rfqu)Y4UZ#pXyqBxK+Bh58QSOKi zIEBUH3``{UuF=hRs3YMiYCJm62UewI90%qdACK9}?9`xH?#$9(^Uf_O#u8@x95BnL z!xABBhP}dN$R^O0p%luUWxJam0Vnru+}WS6uR^E*4t3Mw)IX*(Pxcd)xTM;ZYJ^~fbcA|@4$Ta~BD*A3 z4SW>4K%9h=^-cKvu2O~q!bpy;=0$w(T5-8_!B1ZBbuw0GA<}TpCqH%XL8`9+O@&dl z6_Uz)kY-JxNVN&Y4YLaC=*7GG?Cnf8#QO?jgjMTj&Vw~#9)hRJ=9`Evf@~L~wIAS1 zFuIn4-QkQA4x0J?J}OZwyUVd@I?TQy(g&1sgGTd;%Z{V$3;e4d_1NRM5;amSR^m(AaV}~9KB--Hw9tVKG=tHgDQ8SkBa4k|K-n*= zG&{zj<@2y&Au8WbvHX|ePolt^!I=F-{P}~?w3Cp@G4VI%ZBl=LcckrhgXRw0O2gRt zLce!>!Thly&@Az7>0hzze2U_g{2nA|o4e1VFQ(Ru?F%@1V>4+3?_o_(FZWIDRM@oaIY%nw!R@dMfF7C&BH0PA$KxZ`Rh$@5he z&=I%9%`Km$lp)b$N&K7kycyeiBs&oiL@YBu0Fa6`Dzh)@+^F8Xmv++JF-A#{tG1g) z{tZq_!j-G_g_inZZ=Oc<;=Ov`w^C_}gu!5-c-}L}boSP6wk?d;bm_V`wqj<#hV~+t z)HyH+o9Wk{z6l31_?rn}O;-v)pQGPhsPN+X!%0WTcPEK&_v=kfylZg+{TE$=;kV7R zRmN0@Y`Vc{{%fOtz{~fherJm9?G03*m{#u;f>nDDV?r`J2J%q6S=qsC=+)InC}ezt z-;Vu#&?5Fd;p+FAdiD2UGC(d{V>ce;y*vY9F>SYB2xLq6i079WxbN-u=S4#f67?@Qzs!=hL}_A8uAaLD&Q?w-U2JwtG`FRgmFTp8^^a2gwL z5q(7;hXl^;a>erqzI|?JMMqzaC4;C*Q&|?Vm z^<_L?nd5bI*JAFt8L;ly+)<>>EmXa`M=+>`&FY_OmYqOHJyyLISMwd^qChsx7Q^oq;wzoKv{}B z_Fa#KSiVg4@;#Y9bT!rb*&++c>7~PR5aGH9T zvQ6G0cm0`Rao*&;8l6P=w9Bfr3_b2fv9tp8wvIt-TWV7U=AtvEsPUD8 zDvNI7W(>nX(l@pC-~pS^(Fo2)ii`stklYB8%Wi^>*T+kSjPV=z1zF)2XX^5x@-)87 zjk%G*LGDp-$S=`;o(+aD3MkoI{*~`Ec>G*E2w}t&RkCrMcaKZX8cHO<Hk9%N}e@U-WSQ~&vNngKnH}EA$q3oTse+g2byTb0vbVIz@uu;XbzF; zN;o<1@0YhPTaP0ulOrSXg3Vql@O*Ak*ATyH9*%G3#qRwwmlmU5@U>srFGqX8=&r*^ zJ5d$eU}xpXWC%fs+}q$F8dcMtU{>SXgU~?W@seGxt-Wdd&@*RDs$`9i1O*3`yyUYN z?A0$6ixd>Xs-T$DzJM_NBU5i=2~#7&d5dzq_9N^&p)(_&NF8=Wj8DgOC%GDSTnCnqTa z6N0fqWXft!2}@0EEnCFh=h^w(LtO07G~S;M#+2x*$rGbEb^+ga`T*>`l!B`boK^cu1}@| z(Jh9-igIlUj|b_()tQ?@q~s(d=o%+I{DPVsQW^NRTku1=jyjLNtn|^5eOa>P{?&z4 zR7S8xx0{vs%&#C8BJ|0+Be-Tl8v}_J4Nd$};M6Jn(XtGyOc)|ki{sFi%f!4qWm?sm zaEY!h^SG1d7wJi7o=70Pdy--ARz;QnS=jD-t%Z|-IyCg@Qz68k52H`Ba(CZi6RfHH zG4qtsStsW$L&GnF<~9w@u?CGSna4G$fU8R}j+OKCPE96XABo1nGFOW+R^48PGS2gj z#GbL~QiL{b`-G{<)<66G-@$zi;21Ei&}h&mDq+^krMm#W;PE_TDx>T z9Wn1e1`@Nhw{$+p6*LzDIr#+#wVB9ZI+uI3pd0JL`8)jBk z6=o%(+gk4Kmg33~t8Oakq{s@x?YigMna-NUo3+*F*g=5Lo!7D6>!Ht-cW1T77CpkG z(({K@FMFo{C@3#~%vbj2i?~{$>CBgvviWf}Oa%|CcA@21wE^b6NO2#~l-jq=H~43U zw>h$8s2jsWy52Gnq?YP&xbeK+CO@_^Guw|bEgJnzo2ZE0f1}?>#@XU*EDR~ zbt~oFGl^p3$#$DVk@1|ryHIq9WY*xdPhBig)kTiQ1yiv7ptyu`-{*AQw{Fbw%}wQX zXgUSZ_Sx5LPIPzMMx5Kwd9*%^z<5E?ujc(QZ1DzjTj)@OLFH4SsYeT>MMV~P~&_Z7VAZpuo1&P~0e_L$a z-Q%@bcX7gQ_`I5NS@j}&t15qd9FpW#t)=@zB@2AG7i#Z}v6<3};+nZXTJ~M;nIjj? z2jy(-@-L^bOWQJNL&WBkI-1n@MCPidyIS^ySxoWaZ4U;=Jygrfm%)KlEH07c6plCM zGQB(&hPNM#ULKUt`)L5+s;}#5qrS$*;fs$q>eYA18Xe>W1Vvb*LjPf9swqh7k7>VB zQi4Q=X>K#3ZMa~{N}dKJ9=5zn^0-Du1|`0`75y$mww)BJa#$r+L~z?hN~*0>TT~wa5Cw&7q4O(ua+kJG zo58gEQMx+c&wES*f`gOySM#dN?be+oMq1<^(b7_RG8j2aOHY0=BOx&eIoqF`_A7I# zmt2G5xQ0m?FMSYAh|rp8O>CgN5bm8^?CT5V$(o}VB3|p(#?a(sR_+SZ*-RC7aT(E- zJzna1w$ybj4HG!zJgiT_To5S^Ck^&ZuIas5$UZ{Ht$FTrwYAI$uckCMd+v{p)*U+O zRF4a9LysI(O0`%6Zh`SrN2mUHC+T%EJ7*vkC4Lu z7kX;o*(Wbn~)#U^`TdmaSphK0W;CoUHHsB7YL84tKL-5*{je|b3Q zl-0JtkvNyDWwjo`9jrKJ`D&|1zGf#!9u%0dRW&r62Ox8Lm;ClcR*e9tFSvot2ZXD6 z6;&zrI7)hc=!JrtlX!k>C*N3xQ{cs&3Lqey!~f9ArM{C zuJ|(dviAoYn>$N?T;|JtesAhf;GCUW?+4>8l<;|ea1|&CY?i2S2Z+s5t@gg2aZ!!wWNl&%jWBrV@}|e(b)U&ww-U89{!$( zqP&tAbgR9~EYqtRUk10T+kwhw=H|&&`+3`df-o#t|+|FxgLEM*)1`Zt7&hAk8Dz$p7}1dLB>T^E9TggSpM9 zuesibojJCku0hmOirOAZN|lvsbM?25I;h%g1O*{m+7Wourn;LO2s24l!*%SbkfdDi zzG;MD>rluN>1QhIDw6iD0&Uu$wAI8yh5A*n(P-%yKaH56?4eXbS zNPQdiL#>bfo0_*zPK;yIVK?KSqGp>9P9;!S5Sh--D3S8_Lt|+8;o)5>I29{vq;f>3 zi$UM>36En}x6>uhc97K0jhe+?bdi;VK%Vpcq4y#SePCB|g2bq?c4a+H*u9Z%R$k+E zGpMOhDF(V^pw<0oS{7xvtF!0JmNugjD`v*%KoQrfBkNLr?S0n^9Z>T5J z>(?bUsnIuzi@p*MU)7h?O*NvIxf5rcHE0Hn<;A5j!${;N&>`oBC+jDJ$=1L2Kb}KK zR8wiIcfB~|qpxvDB?qbuDUuDp+~x(~#ckSqX~034`(M9kR;X(QerdC3 zEITW?g7HvG*^>w6Ms<2`%JmiZ_CAQ4pre>)V|Wm}x4+*!X9u1pZ_;M>0Z;RTAj9tm zQr3wg{8-iWSp(&U5(ORB;dnd5Q^wWx>>M1VA za~aLT)L~OpppkQoCq{km8{XA+_Hn|#faba043K3Nsb;@R-b?b8Jp_?ot4^KpXe?W0&C!4D z)sKAGJ-yE9+)~nQXDxn?OM=1laTu3qO!%;_5ODN24IHF_<8N3_J}{7*Xxq_~ibZX( z@hS>HkSvXem6nw*|Mh1)wyNR?z{nx;%a<2nMB-w4r?uqywHS4xqswfmR3HGqr9G?b z^$WxiUsI8JcrW>{+vd#Wg2Wh*9eVlemyPZ;+TU~cyv??4>S(}3!hl>DHW?R5h<$CQ zf%MHzeG>W4XX}4R{qOFAAHk8Q|7iis0@641bzpLtbhA`FX-ggIQKnd&(L80s*PD2% zZG|5Z(X^q%qoPCV1f4TiL3FvMzL0aR09~o6Wdo7dVoG#2+&iWh3(vEuG8t1&it8;h zGB7-3V{_5+|F*3<*}taKl>}2qaaLDsD0HM3eA^CIjBH*bc0939I|Qo{Z=X8X2oGyi zRySIbguK+2ojh22EW=f4sow&R}FBr0n%VL zn?h5BE3PT5NCR)!^@O>Fu`RO6X-M1kh!L?z0!Y$(+0BvgjMp$)j)6cSFY(pk`Jt7T z*J+L-EH>}S6VZ~pFVu99uwElo7u8WTa?j4D@%9Ss)x|P;%3Cg4x0GS=h2#6Ejl?{< z8X_=3#zcOj_B@*~uIDQW)#HO$NGkAeuUHdLS3Lv6*>8otYUlVV#^-}>$O=!5qtP)u zhgK0eqUbq5A>0yIBIy#mh^|?r<4?LAdeq;Hc61r(&Ebl3)UGi&x3UteDUFrgKaHEJ zCczV_Fr#GIaZk#~ zs%}Hev}%fmglOG`h|V+&&Fa!biAyyih_nbIk(@W(InO!IbM~C`@A>QZEU9^K(-@d(vm>^gy@up9tYZ}aGYW9tUK?GN zH71FwKxU!5VbtfimfA&*sVVw}QTpw@#Drx`F^mqDqx;=slXZ#@J4*uNuSiE$4#|W1Y)3o?;X1-zr?Y`CB7VQ7Y%+NwpyG*t z3Y~e9|J%}pbJ3Sr4vaQEImuZ!p>H)(b<694;0wPN@qduWXAjmt9ut5$mU_(5<>+d` z+`Hgo6eNxeS5>`yLHwB-p>2x?P#9-;7boovn)M6d4$NcGJ)gIrzuwG8cfT478||oB zTf>A7u>4Gq7w`NPSjg~={g4G<=K2g)3?mP7u_R!yRc6;$R}oR)dM55jVXGhd+bxph zjIGmHwt&NmS==ZXECo7I9tT3Nj@H%XYxp)BPNVe#+5yheG(1wr1Sbrx+;F!MSYKCj zFMouMx__DLTeT2(u6vn6iHvST!Nr;Lc}%R==8x!6g-W6grgyAiKdVf@C_>oT#LPX8 zGCuDk$Xt?f_3M)sY!O5gOL0}TS>PTH?G@Hd$ZwN^PsTLO{$yfFY*U3OMKBf;j%=EtLD^CZHb8(No$jlmYuj<%Z`;LNt#9>JC>&%3Y(3_ zuA4O{nDOgm4Le$ih`U(06r-zCorD z<5H-W;4I42+K%~9Hj6#DSsDc*kjH<(A;qTt%Bn~FUVgCGI9kFK#w`kMQ^Uf$mz&Cv z7Z$a&PG%f-&@4sA_)R^WoFs1&za8aS#{=~X?o3vOG!|`w{xvyLL{8mW69PPcho|!P zo3nS-53HH1sjIsZlH32KC!lmf$%*s9bNu?k)cFYOnXf!tk>%}XcLy`X(+jnd65Mn* zQa)P}dT^sR3)N^j?%}h(2zfg7_NfD*0#rWmI|iU#;xvspA>HGfZyKtl&lD<3SD&{D z$+h0}ON?82*oh}zED>-7A?A^en+b2fa1Xi}mqs8|q}a1V!lUF5eqr44z+h{eVu^h_ zLkw(>C3sT`0PPVkx*~~-X)$q-+05{;jO|2)AIBS3L+%Ts+gx1IFKK*42x`f{s=O&N zsT_G(OZQ}nJdVFvfEp&+wJfpUnaHPuvQd*>@e*nyBDATJx2~er)R6Scvx>4a&;C;| z9~FW&&AHyB3H_A6J1l5iO4^W^@ljVTJgYC48fV5^dflrfCi?D?;9By_yCm8hsnigE8Rr94N${Cl8D#1=>VHT z&bKJLe|wf1+|z6S82#8>g#INChm4xw&-3dOyd#<3MGQ??cUH~x5kag0viU~)vbtTj zZXtq1wpe6eY5N^2y68lBcb^Fj-LS=GCdsk~U*zZm4r0o?-nM)~ zT3Q;h5Jrhs;g9A{4?ggYg`L*~o@am2v*FO?ys>??xNF1J*6#3(UyjmF^(XJeiQ1`J z{hQ#YCM}%h9QczN{s=r7i|(-!Sg*WbsMyKg)We-WRdb?l@doZ}+0q6HDb7*FrzEQo z=EVApN>LHwgPZsve!lw;Mh1qN0yzs0@FDWk30iUY25dBMP}@<1y-yv7$#4HOGV$H_ zRUAw~uB+`8`uVuXLTv!9P0xZ9s_yW)QpzsABVeY_fSzuDlurU@Ig=mo+7Da124t(B=H@FpE8%zO{h3DebgCD~ z?V-P#PiOOFdfMCeMOx1URD9{Vx~f<#?I2LIGW8ws)zHp%QIwJ3*Y1E0fTgC%KE#W8 ziY7t$RM-EO)%>p%Ne5C^c9_3&8Ejba`E$d9PBBpZ=Ua}tgBoulc6K&wSWbgCH1<$M zoT8Ie z7X8BP_3MgBIsaqbTV`s38{bVYsgaN0?(ynr`90$~oWreMdabS`9efDIJGHkb?4vkf z&NWGT*SpAuQ$Mp6g%lqni5I8yNEUQlm-`($lLIG zw*w|g2V#D+VG@U0Ygpim&C;kY3|p`^W93EYTzR5-+RP1xu~N$Mf3k?ce4t+Rh#!-{ zX#o9=Fnt%zE)xXNmRZ*^J8cXR?-JMLRbeVhxkh&r$zf9wD>jGJkf-|(d6DgONs$w5 zt1RRd*|-o#FM)HmGY7m97-nuGdzF-&_w;pmTE4~k8nb5JiDYw<0LM$b_L~t}>V#Ul zKyFq1eB_I_N$hl-xf@KRtz@5@Eg0g>|7~J0aOfq9=2PQ?@NSj{@m2ypPwNwZ4=pAm zri$w|aczlho`;l`^u%ntL`s+v{$z7_^SrW>j`Jk|P$m1cN=538SN-~ZM!oOh`>gw< zBL>`5^8ayOaq0PZL=o5K940A3N*#y)%YhraxOcZ2T6hMN3YDGDxjRzM`X&Dz-FaK- diff --git a/Apps/MidiController/images/13_dimensions_mode.png b/Apps/MidiController/images/13_dimensions_mode.png deleted file mode 100644 index ec3f7a5920e08770dbd0d908a7d6bd000d8b47d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41764 zcmcG#WmH^Uvo1=M7YPIpo{->{0Kp}}2^QQT!QI_D3GN}d2iJxmjeC&b($Kg|1C6`) zSxw$=-+jj&_s2Qo>|aacq^cF$}u)nm|W{>%fkC(x8rWlaXCgoXy%!mfC5 zxB7|hw|%!j+h}_(@OS0;`VY?C>I>EX_G2rwW|Q084ZlmrN2IxkBOYf-heu$#Dk_SVBT;&ch`PL&QZJ)zsoBcksiFkfk~*aD6z{<+gWWrG=Hs z`W&a0{$`Oba_%HFEBx76&85$P@YLCz_KE77MJ1a0pCQ^dI4M~S*@H5<%H`55(bC5| zJBXd#I7$z~uCSY5nyd(Ftv%hP{!+WZzGBNFZo&G`V+K#G$mL8`pfI{AjfW_u5*WkA zmDN<#)O2zgdTHsy_y&m-l*VSYVm{=4qip+R_Bu(rSGWTno*fx4&?ltmQAH3sJ1xUu zm%McNL1QgYipSh#^_}>v0vA5fnU0>0kh9ft#eJK5Q#F63r`5l8*>%}ndReH-L>xkk zACg(~tcuTCz1U>~%nciHGg_I3{HE z85GzYR#rG6DupB7sOW`KVhG!$w$acdK;8e;zk^FnO@cD4%d^3&6MtutSp(d^UvcMe zf_6iQd-@@7Jb$1hzrhxE)uSIFt0eS6VoUo~`&ENO6EJXLab2b4m2;U>nJ#4Xs>*E) zB1YxAx|B()^^q|5vyGoSw{W%6YTYE)$}`q$u&+TSJK|x2uKrTVaA;{Ho0?h>(h}4> z2breDbXcK_vFI>BZQS~*T$6PsW3H4T^mOW`Z&y#ijh3JXJ0UDdUMJeI2u~E%UrI@tW^g_E7zUN8oC!+;@VlG4>GTFskoImG-` zvfL}9k;&ksxY7?J#sxNX4H^^b%%L2mt&zErj%F9Z{)P+jl&oK?TlXDS{Aa_S;snqy z<{^=hfz!4~_zuB1F={J&nWniWRsro<#B7%B>{zV_)btIFWlWE%nvPmco`O=W5=owN z;X9q@lX$9XYToMgOr4H93FZZFM`GqaQ@=6nKhQ@-3fIomTz1-PH|_?T8@CB77r1O% zxw#x%00oI(`gDJ?KA+!pu|4>A2dij^t$bP*gHZ`Ss1yHjl^`}Bm?=2n@nPXLLoA>1UwT$mso%RTK!vVPS?lP_M zsBtHfz;kI7+4Y#sE-Kpoh8m>2+H z007zepm(-(jSEiV0l6R6-q1;o6HpmUU%{urD}gsn@PY0JNs4Lz8fFI-uHf;zfJ&N4 zQo@Nh^J~P#omW0(fM|tW<#c*Mqlb5aD!poT8sheQI`GZa+{UbLQq{GLjgCnCwD2euF|d!QN0$&Ebe3b z`b51Ccfl5;5#KiP|;h<~^pb-nsLw6pC_vDO}##YOu4rPG`Vr0`1pjIA8Y?`n4dfV|p3v)9ho-1Hyej%k9dB7Mb{1eKU*PVI>AY@U?3`6JyXdXH~pF5N{ zell**VwB6^itOXGs}bQT^wJ_GUhvfz=)be=`%K*=EIUcbLng8 zsy4chG%{laJr)sm+g;0WrHeXN>?eNoD8Lc-F56Yw#>d?j4m(6bmjmKY(=$#ss_mQ& zj>itZnhaKS@~GwE-_U2F(^+JPrr2uU3-IPE>(kB* z#UDq%@;%Q6^Z>cQO0@ErJAO(d7<62h+5=XjZ)a-~l|FuCxJpD>_k7BGX4I?tR+^B# zs_Lw1B>!l)=VgydH+no@Uwo~tQ0>)t$L(+Xh#F?Bzo@_sQ)f7UtY`x3$s~ zlQE*1fUP+56^3eb`HZ@--zXnL0_Zxu&lfW(1cn$G7-D_+Q5${lv1TkwePGr0}r=u?d+G5)$?blc4taqk7d}T^A;c+XVXMlW$-czR_$R zl5PCH+~up!=4~kneGwQzUc*|=I%D-}xnEm>9$~~8?DNZB>j0xrS_5_Gt(Coijs)jJzhoF2-D z);U+Qu;>H9P<8nSomz5XivG%5i=We;2?S%w{S;Xc)?EGiHU&ui*I~kvQ|Kdpot9d4 zmTSRnpQJvjjmyPPfD6grjMmh!6GuGpQ6E>ead-Apx4UYq{$ys~h{3EE2cs!gKil4w zq5DBF>3uGw%FAqxlQLyZ44I@Q3!z)aaEMKyU5S-7pHp#x>&!*hmdZV(a+Oh!X;}T?M@V54f-B?`AUAXY&QR?e9+$#?NnC|({NeA=8)Pdfc9UYng_YJrm`ay$zI|k-}Z2u51Ke}3 z@2|3wBhX)cN~gdAaT8~cACFBVoi#Fz79|4II4I;DBY8fsA=mCBZm zHCXsfbpGB$?BCKYg%&X~x#|Pcn&pM=Fy*qA%legYzx(ydsk6caEVq97z8@DpQp?iy zRm5r;vdP#YWt6nlE!lXmKo!^8DD@W&GMpvrToG;z?q^`PA3A)pNe!6w>viFW!oin4K8L_)|waM=JK6MUPN% zU6z)4)-O_{grZ$$Cgv^jEyl^Ke*J{sM2zye>X4@4yb(+5rt0c=ZblpyaSJVrv*_Io z43@q@ZTdxA&GgEgf*$cus7Ar8K56|j`5_;rf>*{I;&gNgx&X7$kx!8`6rF>*RAo=B z$dB-%EI>1O8nBK|!l<<{o_R2mBEz1Z+s|9w;(FDLGeK+MO;o_VC10s5>!Z(mu%0?^ zqM4g%p7zvhNH6iN2q#asGjcs(b8`e54=>Zs8|R%Dc*`yL28L`&Z&3D7TTYn2B~0lXh^GsC7_>%ZA9PA8R997^8cu0O zfnSv%?%eoR5$0c7#a*-O>wVV8-n8vl>z;n**?3d!+vh~AP2!{(AGa=cXUU)9pU3a( zn~{TFPCn&`x|Z|R{pNT*9U9v3$MhS?cV<=Bek5=U615Y}aH^>I_3j^A-3DQ94i4E0 zeYb@NCRk9YQK6hgtXx@t46eMmQ-D0#O9>{jB998XnLt1VZxW@$Uz%`Wi}kcqcaD(%d^jD;VPx&D&?z=9 zq(#0@*DX)r2ik3y-=cfA@PYQbhe?BPV`V0t={m!qC>#mJQ2z#3bDc` zy(Y*fp^Q6|x6{j_^4^q~O&C6dA*j>C)~h2E;^gEp13UcIML?O4mjy3^okC)|7+8ts z9$w#zHxbhyOCi@u(Ydm@X$OA*TZ)WP#lIv4BI(!k^Nw7TT(%&qE-p{P%g_ye147X>^T?NGWnT}CycME;i!bu zy1qUiA?oz-SO*{1zB^kW&(2}L^}Qus$&eJ4_bbNASAF3+&=-vdigv+Bxhx8!ZB7)( zw1!u8$vdCuQIb(N@lCi1EZqLgEK0qtu))#b^WFAy8}!0IBfDoC)RgI(M|*hjQ%CDzQ4WPt^hJF|=2L)+|Zrb496xUKiR zPYt8X_3&VOwh|Vq>I{XAb~)%y8X7qnh1B`qkJBFaEl4w&`sEK>SGU0Fn>Qm_8Ldzb z{A?;$Tu}1V2(D#DBd~SMPgZy81qmE~>=3Q+)6vUhCd?+9Oj^9NdMG8|?!AFAOrxY5 z-25VA-)z=A^-caxK=ozdZy37EC5Icz9d#XsLb~g(4(8gaeVR8hMBKN~ z^W^gwh&DQ=^{ul#vR4bB-8jAcqp5* z{*0GpA|@ja&h_oK_&t6$XatiN_3F6VV95TAn-0D5Pvj`mB@eTEd1tQ)eEpxcjeYyr zDpZ|0(x7z=Jmx1#q9<_RcbM1A$t_6vBJ!~&zx3v#Kwm{B;^fH*H0E#K;HN>UC$F~0 z1&RaQ5!GO*TiK*KUb38qWKTyq-tX5o?9~=kWDcDUUGE%k(CF8z$BqUkV^i960$STa7m0@ot#CUxX&qG+6BN<1}M)efquT&&$!{E zOg(;somG$YR~ zY$LxioF^F*?d#GGbABE>b)juGdzUlsoJ+LDG?4Vowai>DX*pJ6YIW{JknL%jQi_dT zB94`p&J51v*(Hs-FP=3C--NB%D;0+JhN2Q=$3MgWT!}Z503=cM?6f@Z=*h*j_H-lXjf&hd3kKCo%5Re| z(jMH5L}ixODQRZPT`qaNMB3q-{Ov=DieaNJ-o`A^G(eJUPJdKm)0c_o5DQ1;3!ei4 zKVf}D^beDO#nYWa1`Dk^z9W1|iBiwTkckjK-ZWl27DC1y#zow*ox&AfHf7PjIq>7? z0_iTKzwlkLk(HJCiI27++h?SEaO}GLVpyQMD{gH=lyea6aOg_-%R1;AYywY&gBL!F zITvZR;*w*nXXJ@@ddtYO+~hSatf8Y!W*C*#MwGbSF#Y7OH9py#viMo8W#KMAbR31f z@3*Y}wv?xp;s18kf2}z-{pONfuRyi2n|6Q)hC&}AuqZL-x}@v;iSFOhk+8BlZ8rMT z=1O~aY<74UWlFo~+yngt=h}2hN#RoO)SSpV@@5!4ll%vOYlrfz3L36%U0Y%!uMDBx zCRNM0&ye?xJ~!hNW^!RwRLJlTbjR`Vi-%ZSPcF3@cDx~+&im52vk!JjuP}t*!#m6G zbcV|mzG>hFCyP(N6w#9 zd&*`!Z~MmYXA5Hlh{Tf%|3nk51Hs?!v+1N0X=Z5|87V(xa%QA;Uirj}M@BCjsvI1* z3@^yV;*$Jz;CD@x37H_XkBzyB>Z`Vxrr+a7x0oPBcL)%T|6g@;RFP^Qox+R)2=S%r zs**9pf$~RdCSoTbK0nABd%s!djf7v2Le1xRRsrnmA&bBqbzLq;Jn;mmp4;0refs!N zjz^gf2=jl+2OMGVvHqdA>JMI#ny0C&?C=R+1xS<#f;n(Jkk|C?^UNlYn}(z8BzBB> zae&Vp&xi&(z&fO1B=q{?yL{`C(ez}|->FVZw~DHU zPT8PPHPoS%I`{|ajRnQ|bM`JzKjCQQV@jiK=-yFS`QT64U2&NgD=e8h`IoFHJWV8m zQ)eMYOK3w9ET+7EEukTHupjlH|1+C5b!dc?O>0G-QQ@K<@)@Z-M)U_rBUB5c{5xe zGX6(t$~l;1V9CZ4&qf()CPw2_31D8~lCzwR8qB@h4iqBUOipUz+QnpTsOD0Q!#eo7 zI}hl-{4&%+BA79VV`W}qzo+G|w^i?5!wqx7w==5_U;<8JxBmH&QcXq1q(*9$x7_OZA2rWbt`} zTuoJdrwhE@#e`%w|2W$)G$U4=+y3toeHu z?T+{VwWfner%XQiH>S~ECDvET;x{%N0jzX?R!)PUB3wH70dT&n!00b+v%~sB507u#yFc z!c*#vU7Ia_J|VO5ihiIo5%rz|8Koj^DCCvx#xf^u!rj^2S1x}&Wrtd38PC`GbRWzY zp5uJKBaz~FB-#k{253bzy-J+Hb_Mx}d9%}cic(VOo$>Vmm*dio08hYRqjU#_H!ZeN zI#w+j0xSQ7AcRNtfc{+5=BaJ5n>v7{K*zFa53p*dx};lW9x_Uct4!B;lK>qk9l?## zBJaw6tq=ehx#}TJ7#m7(tF+H9mo_&--JVP}8kjZCx?GQZI4&#r{URUdh`gB+TFfhq z5eS}G8NWWD*FVuD(klc>_K06vNJKBP9A^_5_2!!v5R}dI9bK0w{;sa((kiGuDv8Ov z@@dwux$2o~N2fU+-*EdeYlK4VFCP;wZ|i<-XI_s#8dMqNYtlUW!{fjBH(E9^isMbF zW(;(G4(7RY@gAH<;Y2MYhi8jz$!|E7veyr#Eu?BlesvxHurR(L-a|o7RkwI;543Zs zTYuZRVcq2%C(k#m5>K?eN{)G~v|FhsZ_aH4KLa3ifF~iYGvjHq?pRs7Ol7ZP)Kg^V z0UPx#(90hGrv|83mss*Y=Qba<6UThZ`#HxdPwa3eEfnlm1F0I&+*(V}S7(Y_W>k9W zX_!f5-kk%Ihim26QVu9Dd{n&G?I1ukhbILQH$A8oGVHIg-Z`^mOa)h&N!r=OTEY_&F{ zq7r$g5b-3cgGWhuSez??cQbWf?ayRQFx=%c&W%{9CC#WupWQ*$ z3Ato@2+ae%OC)FHhQ*kEw<15wkam_gRx*tz<-+$UN9BXJU?JG1P9;k*6tT?w?2N@Z z9SE3qVwcByEQ&_6$-^}p-eAtn?zcKvv{l;qZ;bMOYBkuPZh zh~D7;f9&!aBRY|!RL*enNq~Rbb%FCF1M_`b*`6^){*DgBiE|Sqk2&o(Gm*p|3uAxCO`rcf;Mh#%~` znEx4pMXKZnJonPlFO?Lsx&@kL;meb$`4t$}Aa2PY^|%y*_2rkFWpB^KON} z*`)h?W`C3hI<@or%8T%>EOEY(eECR4Ma8EZ42&6N1{gn>z1P;Bst!Fqb`BNcM_j9A z_>>Z@!$}Bxgzml@qkR#iy8Vo(@0A+j6Xw4@LneMItkHHAF&>KoK0xgu?DY<5l`Y{5 z*Cx>xHy7y!jYn<~4kR*)ieem;+5NQQ#E z`~al=TYtY`T|O#nfUh9kKaVC5)N`3ykao~GkE6}2XyyDtgS!0`D|zJ2PFMyuy|UR( zAOu(Ba;N9{**X;D<7UaU%kQ=~-?WSSEEJ}c+Qyis;%^^{i0cH-n98D!}5@;?=dCyadWK*Z_MHg{Ze4ZSpuMhkh?W1O&+hf6u-9xZv9alTkI=ujz)~Mmw&*vU2-Inx zWJ#R44|=Rx8X+RVHxnw}ueD6d^y^EsOV!vuGn#THn69ttk{|k%19=$^~u0)K2lc{eZ|Og$+rLDu1MrMtxEdqa=tzcn5kb+Kt=-*!DP z>7^~PM+m#!rqy`jm()Gy#H(KRcCQZb4EN<4S7*wq?9}!A-qOv%ht2aWREZMkX9|9eR z?T*yIosJx~IVl7p^M{O7<7axghs7vkV2_nJYlnhi$=3X#k(57F@D|gBe*yD9chJWi} z6%c9pDZM-L*MZHNu}lLsHRx~1qNDNwjmvMgH}Z|!5P6Kx^X=@*IXJ2m|b z``nl`=5rh~78bSyqT1mZV<{Xiu4JGH5fWn>t9e06r$2?KL_adOw|aUTe% zQo2XkyBgWwVAsSc?8zDZ4G=AOo{wt>d?h(B{w^?ga7NvHfa|Du;e}nS%5IRJmJ$<} zP;9KVhOeIN?w8FN(Be0b9zpv}7|#%eyvPU`>Y zX8*IM{42d#^p@T0gnE1|X%|a3=G}dn$@iK8__f!9er=rZyoj^~rHZpkT@}jB;ZYWk zEEVnYZ`=9ut6+@$X^i8RJE?dD(~wl94!(S|G$z(aC-q0xCvr+-Gopi?@5+Au^Z5@; zA;ydE&quDDt7DcPlgaIm5;<;e zy<&VG1swWmtgR6~|g;CUM_)5>V1&_b_tO zI8j?=)Ov%|{x^b(0R2%AW`>a0>Gr?E5GRkyH7O`wRZg>@6ialaiIxWYYBI%Cuyv4Z zuyb;~>aeD2yxZoJ!WXsySgVJaw<)Xs5kOnSw{E>;-uFl%`?^uC>t?YFKMU@3<3kHb`2wipC+bY3+^R95qi+> zF>OSXc_eP=VgAMS1#4>jP(3Ev7mN2G@*?|47b5H7_Li#&f3(2cvo^o`9zYaX0DHHp zP@N=7FA92?CrZCEZQ|lKU#jwksR1__ur7z5hCMijrx7dkndv?H1q9#!_q|3S=uYZ2 zG56OVe4)m|QmJ6O*N1L;zww@T~y5`{gugr~(TrnJ^$etK}T=Xcvp_dwAdrJ6vym)t93W&2;@C!v$S>CnC) zF)*Hd*1PXD>Hj4_W6w{%mCnzv)%4xamx+hmQPO{2g}h2Uas2ya^oQIM*-yABPk-%i z`TW9|G`4>_i4CZd+`5j3(M!Z))I@Rm3j=KtBq}DRnb1nb{o>Qo_|+$ZUpsK*+*rSIu5+CR&N0>3E7rc5#3s2Z&R@)oNyL)x@;^=exvIz3+Fv_i}k9lC8+TP-W zP?@qu>KtshI2B^b1&5yZm&S0AL!UXOs?|NO$HMILDMN$fm-a8 zpFAzr^DMcX7>75foo#j$G%73C6_YbJm)8#bKIC!;SiRRwtk_(pZEAMAehA^GQ>Wx~ zHU88p*o>BF;onkwz}dYuau#WaeKyDNI&(rEq4=rkbe_Fzsb2{hRp2s{&t_ur@Yl4@EWdipunk?fy&Zk*PtJ5 z)sTgqPrqWYzLxQ5`WiKtQNjZ>dt@^u)m!Zy1wTo|#*LZ&kDm<4_qxipu_yTVyp3IM zizstvx*`+h@@f{Q^cL&`MbwIogatot3d^{;4MB$l1zcAHvLJm<2?Mgw@H7<-P+p@W za?)Z-3oA8!(5fkmx$OMs=CTQF<^h4z(J~Hop(|HZN>Zai><+)uQQ?a7s4ZdQa6{gS z2*3&+Qpo0?c9mBEDRB;cZsP9U<{HH@dtDKaTYKDT$j9R=3It|s{I|B^fo#7nMuYYN zc9EqAF1Mx@s*SA;s-$6*Co|Z33K{}9@x0vrTNzwBV~6)8KICOnO)CSrRyCwCe~GA> z7@!;S2ckeOqy>4xvw(aDcOKi2JIlCmvr_rdp8F*2LGF{L5)F*kJ0C#xE>fG^m~wQ` z3oQA$IUzzPiW;|}L6T6|q0{5X{am}e=5!(oUybZrt*KJtcW-14@;ofae91tvy1Lt8 z2+Yk80`+C-&WzC}YN~?%!Q>&&pilTK1{80HwyxDy%pz?PTU-ZQ4Fsg5c`)TRB%qhH zK$hGe1AqWOfXUMYuM>~YLvP$}hL!H?84&ESW~+*g4!|+S3h9}p(-!w>SN&2VAkmNFaK92r0F|1g;HT7!!aIwsK2#^6fQYcsMIS=fZ`lVzzB&NHuDI(2{jfdSX# zhfrQ2`82kEeJgzzI6 z%r8FHnjXGlha+RlmNXM^UpI|yC7MmFs_ai~URI-Uc{Us;DT1O+QbQljkHL>suVN<7SAE zr%;ARi3T?@;-twtHSH~EsA12ZLMj9KYfG#7bz)LiGp}dgac_Y~f!Nm85N}g0923T8 z8cF|){1M%8mBmdQz$>Z3!ooZ*)f;j*)?R8PGUOwv z+b0#~hNPzDEjJV`tUpj&C&1%V#H#C@c8kEx(jfd8y+S|9O4q$49LL3^AN`}Jn}^(L zuAgU2O<*}VwK1uV0&Q_bn#U;6mE&nM3=B3rpzjZ$?jE2sEEAhnooIZ%5iCm!F;h6 zaoYLDSJ^igDQQe33f$|GvQlAJJ$@P~dpl~5UH!T^Xv4^}T|JY0OGTC;y{>v8=?S-KGc2)nzcaS&N;Yi{zsMq7qoAZm4_pr6AkhMBl z^c2#uBv)FCgomg;(T5}8N`P@V( z3;!9^UKEZh1xMg=CD)alVF=nwxRsd+`Zz#GBA$QN`|_NR56ky@C9CDCq5B;A_3J$t zk$ePzEupb;n6EJ7q&Dz)~6Ry6iRfl>WM;E_*(-uPg#qw^$O7qD0~cGA;*(1enK*2~s6XNo@k?cW)F* zyV9aOUYW3T&f5zVMjGun$@Kkb@0xd<;~n+rljIa>;r`?K-q-w7t6av|k#lOx zuyamPJqmp#+K~CpOW{4n%e=NgDs2}}8fbvJtbtLBLWC(B^#+iT2D#S&!Z|pUTO`-f zTME0x(90FB`lYAq!>%4uNkelEzvi>s^4H@5H7rqFVes38{!D6mdRNBn5-y4GWx=`# zvhu5op2}~_+EVGBoOghUn@u+nkP+rEhG5-9w-rrN5r&9RIKd&ohDz4rEIe9*e)P=} zZT9J`{|bIUsPGPPOmiL$;HXYj457hQ!{*;{8OYZ(a0?gT@@#3$jbOdXwdxCE69l;M zX@%=S2G(=ugn{8T--_Jq&hk+p6Th^^roC-(Kk5P?`1bfZAa8|UI2Ysza}k=PiokFo zQwj*CJa~y4Gz`~h5_`)ZWAy9%ObDDZ+BI)jHQj>zsz6Ji(u#5)qI#6zZARlq)%|m$ zsp}C>3Ac0Ux5*&MDUEVa-})1Z?j43XN%W@d`pdxjOHNOpGB=FMO5$PqHHWuLp{1;B zZ0gxb2w_|o9F$)*?ePD}RR?+_N@^ax##Lt*!2P@t$HyC+LG2D(7CZ{cPXUkGZ}!k9 zG+jF?d0sYw)<8ECE7U%!(CICW1=kSWPIQ2%=AV%%P)F!%iGO9`kCrt63V1fJBUDHD&!g z0aF9@L`5@4)4?vrF2(Mm%@OVT-EKuLLZ2ZqA#X`hL|EVHx`Q6bESSrM-)P`GLNcYe zI7*P>Lx#cGlL*R2TZ#);cV~s*4vUHrOS2}jmT~6;4oXXx{`#zMK*nx1`~nIkfK5!0S~Y(Y^i5J3(;<$Hk-gP>E~ zm4ICzqN|A#eMo7+b2c_tsmJ%Q>7cz8$|gQ>-EX+EH*2p77$#w%k`r9(MKf7)wawgO+v5hJS>vS^II5Z zT(Z`o2T)%%{V6CEAsi<)$MEX&7caEi{r2`)iu|0Ghd{&s#zNKmJ4b31IFOB&sdxe9RLA^jv4$b!AqjZ0OHFjSLA;9=8nEe}8wN zwl|#pCKF?+%?MqM!vSEZE*4)A4R&8S!d-|K@o(OZ^Q2~GTf=upGi-fa+FQQB-spk`GnN2h6XRsV^2(g$i@280ja9BhbgzKu@)yJ z)I?|va$@dwAmq|(8v9w|}8E)NAb^BWEWfEJ6OJW-Cygbs7R+EQd`W z9_j%VMRVe99AVF9NS%k#KVb-pJX=u+k;Da zI6IdEg%oHWsI1%bYVm1rGhFoUEg_m;TlRpzA#n60T zW7fy?*j-E0!^@_~+Lm%xRM-py=ho=E5>G8;a6Gl?$*)&ObN2f|TnkuO4s=u^>$ON* z^o^j4k|`;%>m%8DmR4i!elrnXy? zvH&o)-K-G~Kh)D9M~wd_gd)5Eh~-2Kfm{{J^n*7yB%hYEeZmKWZ8|$=4i91J{AyD9 zo0}GKaSg>qOj;8z1|5!Ukk#)1_=}8Z6zub>fs9zG?McH{dq@dqxfiwo3bH~3VA!26 zrN9gnBA+IHnrKAlMU(j-_tQP9Q$5KNiKCboY3h-Qu)7>eLML(Ysgy*jtTbmDvJg$7BxtDm? z<78=@69#iRpX9Z9BivdpYYduWJ#S#raMqd0S4kr;xLz>oUMlk8mg|3~o^C1*2mF|B zh>FLOh1y&cFL7xzc&pbao&nH~>~h5dTf&Onh2Pg^VmdW94@y2urrU3T{Q zsuu!Q!nB}F^PM)_UW@N^jkjGXo<8h=H^&5Y)dtWzQ+UE#$ciJCq67-o&WrV(v#pvfUociKoc& z3Sj?=U)^$MxMkInnK(M=g)joT4#^$9luTI`I6xJFUJih~^PVaRN_1J%Lr1jx@ugrN zfNUK0-XKCT*MR)xNffo(O>ARr$~IKcayYT@`zbuRrpqun^Hjoq*UE!&FvK( z@4KrHS{cB_Rdu8|11bQ2mp-e(u8b*6X&t5Mb3r=Jb%09X zk+U_)QdU3WBl;W|A-Vr-XmGna)!S>*xLA#;E3v|3avg{-AUgA1{1&^Upn+b-U9;!2 zDNOyD*C#fe<8&;0rm*o(fJbR5;Flk{D$K8<<_znVZd?xo4eFDJ-Rk)Py7vfZX+1;2(AXbr$GTlR#NOGmC@C0M;yZoZcGrE5q#XL2+ie zvOU(BIxmCHTYz+EO&q@CzjhI!yidP(ejVlfs^D9Mx&S4?;05p>uGv3A=o_`OIeYq& zwaU)#xdP>PY-j8U81E+Zn;rq!R$pDcR@b+lYjOS_sLm1-u7A0687H(!#Ireg)YOc9 zk+VjBWYgKk<>bj(46$i5KyL2XOew%R0Yt!Pak#91^Ib-hWU>6~y`vVS*AoPjAkcSa z*f+w@n}K25u1HH}DZlFg@xSmV#5$xFAfENnLsdpxA)V@|(lZCtgb$I@#115Sz`)Jw zSn=eEnPvjoH;O`jN%Tj_b%hU1EAlTtYk@S0Am54{eW;i*siX4#s_=tBhY1TXZ9r%I zZSkGEW6}`@%(x~;97V3SuTZnf*6=JVtBlAh)5@fi-o-Xf`MGE2ra??J^l5}Q9|aNo zX8z40F6C@(Ab@(YFzIJ~eBxR)$(mRAfm!iG4OxBO>lZU?XmT|z7y29sd{ckql{`8E zU-NP=B00x?v#h0F>-vfc2016Z%%E>XzNEUP#_goX#l_J@iXe3sStpI3$w~tg@kQT% zHs4?%XkNUVVx^PKckP8LNGSxO=A2)bRpyR826)@+pC)c@pU#{r@k?sm4VwYR3F!YN zcc=^(7|v5+cZu9YYEZ(FpGxxbAWcEuU9fRpmGO;(tKd!b;Jyi# zit-)vA2;r|Ge2`ERvd?DPNGi1zV{7@%hXg@{L&!W{E$=N3NH~)3*2;Q>WS((yH28{ zuRS-1HI3C*N!n~KxI|f2+r!LtMLx92eXWuEnyV_JK4L+gD&1C3l_t(|5{X_GH;-N< zB#npfJ$)Z;xo#jr6j}8>@LebK`D-N+6)QhZl_nAK_Y}nPU$mG=#7DXkZONnCrGxYl zXOi5ry*^yr9znl%ZdPyB8uAX=kh;;%AG?_=)Waf7Gp8-V@|rhzd0kO5cOduPwd1Mh zhFn)88ewMxbmYEgoV&@aoCLL6Ujt)=@?BzI;8<%`O=l@|4WxDS`5^Yw8*9d_JRKNL@BE!H}{J@EPXu;Ha|cI(ebjabgNN{ zlF^N2R>fmZxVs&peccg)M6tAGVdZ31G%AkoP@*T*Vi7ki9>KJRy$+EO!v=DyA;_)Lb zOvwPgm07P4HiB9{0wB;y-${UK=&#APi|Pr(jF(ReDZ|4h;KzI;mgO7_X?ka#145Ac z5}6En#z-1&tU?uqOq&bmQ6qhR<*~6-l_N@}CM~ABAVfqMDacwc9I}q_Q3)$XO`OH*rcxeEr5#WhsTm8gM4QY);c2!<7DC!$gVpxDb41 z-_M8&8AeMh#+m*Xa1`G)+cMW6?2>CngEv>@y7082(H= z>CAn%|LfFJasz*7#aV^K|4s?j-=`+|`yJN*cA+Y!guhkY``@lgUHtD%Ts`FfT=V`# zO${^QR^n6|Z7{vUXax5F&QXrq(Z&&6`G`Iu%N{oX=h){C8^Ky@Eto$VlmdEk7cieZ zK9u5)kWh=H)@&y=E>%l=Um3!}5huOQh_Abede0cz7wCQmQty7N)+@f4j_x&Y2VR-+ ztwZ1J;7JW;7LxmvzV5mZX(V_d3k32|Zy+Id>xdw2==#d8&`FB>5;AOdWbRNiZ7GS| z(5|D6FBtyOaua~_r(%kCQoHNL|+J=oVh6wsLrcXNBeM@`BP!WuayAZEL zmC#>AZl&+((U5ax$;$HoESmiXux-7Mf#E64J6}sB8nZ(PG1z9& z2=i|Vvww`n{yzl%p?r%18nGSSRPy5M_37)24#_X)o!QsP@!$UsIR(i8lAX85B#KLB zMEpd(S!#X&FJM8d{szKUJ*%F!SOz#3iXO*0f!~U;D@=yrFW7z5ZLVeArHExrxZCv2 z{aFH75|IV!$y9jHos}9A6)cOj-@q!*FC7`=rHj4a@X+}T;x#QNgiHN2&p>tH9rgM? z3@cS$rvm)aCTeVVFed2BF+7M@Z6>)t@o4C8x?q|)=77&1Y5MeIy(|OnOG6-s(hpRz z{aaG)I#W<8P$_kPps>Zz{2-zf!h%5D<%Mu7^C%PKNu}#G@lozc|L9=5GmVcD3xTY~ zg@MUKaCxCfaQ`)On&WxV|n0D(?Fq-0jtHgW2!GGZ!K51s66>j@y!;Ei--qAB#SJjJ znUb`9Wo0$p*Y|74h7d>EEXwC53A2v2#7qjr@=WEwEiE~e=RNCv6TH7_%Y4O^wfriqCpHK8AR-F$lyYk{BB@T2Q zwwTRc^#n4Qov5sP{)}Q+$D7u=kelVYtg!-ObAjz5ivl&h>IEd=1Tt3yCi0Hd? z6yi1-QyRls+Zh~!#!&qTI~qm8MhU_DcF(*^eMG2=luN1Agcu#I9vn*Sx(=XJ#t#6{@i_8u0HCJxqL75%pP-h0FjdzNKiu&&~RVB-Fhmjl_^j>hVR7 z)OFp43nC^X$>U<3)43|#IEzS3zkCvp`FWI%>$D_Pf{|cY+JJDj$dn*uUdL=&iBjHS zlrK{wddr$X$ohw8d{Oc~bqgv3)v5IK$j@!eKz-M%yu>#>PZ_f`GhFTrpzt(81upl_ zCzYqD+n6HC`FB&0o8}9s%yu+$;rUV{lT&+fNjs7Ck*n4?zwtAmlM!b38*1lmI{oM+ z{298aLQKAL^S=n&;yFHIE0ru*n^(+AdX-Q>34suZ%TEhZcq*m^?ElQ2tn6wZ%Bg+> zlaEQySs^9(3J+>IwC??B=<9yHu~f7iEKqf<94Ndj*&R@&Q}(2p;-}3DXO|kHtaVl3 zm#4T$+~8I$m--Q5YM}qvlb1#=OLEgrE@nLYv#pFK zR9TDIszM!gYoCgJ|edvflay&Z9xJOMKt(A;&FW+P@)E70G{ zCrsi!_6#m_vp*5oiCnPDuMuhum376kBq3Z2PD0L%UJELi+(LIfX@6mtE(+Trpm^G^ zSRPWS;q$W7w2oG2H8E1fct~s|O6#RnkFD%@yfCKcY>qwNN*6wfFScU0=EY1T?a_@( z$<3%^U7@(&B5ill#aVA?qF`g-Yxg%st1&;%e-6%!ZvTRwKLqAS8j54wX3?{vp79k>Dum+XRoeO7x2gdPGGN17`rjux^6M@F zrEPQEt-8)N1l7|sK`U3y+DEOZuK zV45G~shCGrq8=Ea9ih`su{m*sYrEubJP?!Id#L;HnY?|(43>wBEto8_A^`oHX zIxVmKL?Yd|A#5`7R5Wgo>BG)EZpxS&xElR?cOy_GXIt;Ttt8JGgsS)+EAF9LmUz(v!=aYp{KEpy?&z`-^Wa^z)FRXBH;}`Elb8aDZoDcXMXINz$ zNIt}q1E8h#fnzhe7UQ~;R&PK7WWk~`RGI&$H6&5gdF_PelFKZwdDC9J?joV@fP}bHQW5~ zl=OMxQ6jcKv7iE@&Qhj8Ppzfgbh!Aq()zjU=4#dPnMMJQTF3bC7(G+VtHh>(q#+I&5)xFTz`2X#IsBf|$@t3K zmMT71wNh^}-6)_U`{GpHYDe}tYuzCJS1Z+{QzIfElZ;T=w8{M!e7rKMKuN9Ds>1Kd zv}1Ov8XkUu2W4vcvW1)n)l>;?3VW?O*H`WITDvZtPS1fu*~f$O@% z{-H?kXP?0HqJFFEG&@vX3EHLH@5)xjz(7(EcuN>QjM&^@-gE9+ovrw=+BN8O;Kl<0 zlGnhF>f0BQHahiMXZMay>W|04S``L0G2JXRt&yuCKTf7oL=1l|exOu2SFmXMl0Re1 z$yY7W3b4_9s!E5rgXa?`CWM^CB#Y?u{n_Qt?^s3hhGTdXuJY+3W{23!y1f!puEMmT z%fVT8;Ex&)Whu-yvRs%tmg(D`UXQamPf|L1Z9PNac%5r>bh#QgP5@bKKp=?qbQ1bx z{OZ*Ztou39#`W%;e;jpm`Nm9~>8SYfs)4|T0dQDx4&JM88xdmnHXkk<0v2^z{q!vb zd_xQBS7rSL;ZIq27#l~b1{?j|7T~r65HdD4A(+p}-1VJsX3V1+l;9Vzc`I(k%Aif>T!Rh@g*MRT8Xwp=L4_?598oSLPf3v-HEIR=ZvVqB$q4p&y( zWcZD2r|t@7ZN+#z`l61gO1@5d+e2apSLd_9m82hA?H5_l@D=$=r^X`XU-^EwTp!yI>u+M)52PC?D!1q zvJz`$FxZ8opk`q#2o&&G(7?nHu`tdgg5W>w}3Ul=4=*qTpH9t}w zs>?>a$#tm(2$eXE)agcwS(m929!rHV{jw2YEin>?N;OZ{Ka%Lyc%EfgbaW^qv?mWH zh=8Cflzbn52Fbe2@=er!NQ(I`_2Y^X#Pz&UjOY#(#~zxG_ixD+#MltHa<^3xQ)&@f0kO*ZN-P|w0P@xG^xxl^V~`LukoD@6|cRo zR^#Mk*e%ANe^u!0U2?YOpVb##gV$CxTHVh|q6~CDUl&w9*(;;mIcTg@m~h+-r)*ik zvs6vm>J0P|!P?L5-HCi7)A3}3h<|q3_Ess|flum( z(!eKwE{6ZClG|@{0OR|&VwupYF2gHc%9_Gu{o`V`-oO=Mu+8O7v&S zXDFOxlKOkyzm!xdc*qF#_tL+VEO-d<@=s5DkInZlZ}!EaZEI~+D9;Mxt!4bQ!OU7& zz`{6&#|UL1qxzs-0CF67(vpSDw`4GD0Nc|oz8CwZtzczORezXC2O7I@gD-!%cLy^2 zIZ{vaIX)>7k^FQn;r`tIX;xLeesgWXoCsqPsFk3QMZxh7L`+%D50)XDlvp%Nf+;YA0G0onxi@h~tQ4TOfyWscDeqpM4ks*-AeFRRD!aCJgI>fa_) zm>Unf^4rC0)J8?~J3Bf$L3}6^UV6jB`uLUIO2CE)Yi1z}V?D*a4vdywRQ9FrHU6%igjo}_-(hkRaKR_d~m)%HVJIn^wzk$9})QWP>$8@{Lo~$hCJ|}nxxTp zdyD6RY95@vE%HOvekX&St$9|GaidIPKq5Dc-UaHi&@2w_N&%YtH=*NuZ|}<9{*yNJ zvP!tM&i~c0{u3kq-z}z^l)3%ac2WPkCjPHl5|Z|Rw7P$CtpDU!Gf(fi!pNqYowqQF z^*=H`>oAL;EBf5v&@iSyph8o202Rc314U`L(v7t@uJ@gH3}2yPpEnq2o)7yh88gY_ z(_O(dqR3Y1K@QF;L-@kzb1R5h5vuH7V~92>({SLSya2fi(V+ zQbCah4Ka|36#H_!1r`7I+dzknXcn;ZU&As;E5EC=Q?Dj;Qpehwl^FMQrmwypf>0A| zWyO0c7=9;Aml@n*X__p;12b19M5{bZ?g=R0lsjxq8?|5^>w-THq3-s%->2`T9}WD? zOF?SLI3wto(C76Fcer>&uAb&42{s}a20LTXuI&b;(f_&hkb!h;4#DsPiL?Rp-(8z{ zxfMNhggt4J?oQLM=~?lNZ4KE7^pWj=MY7VXhSQk6?^Sw^y*Mw-kUW|zl8gJfJ%T}5 zW%RBIy4^F7za=MF0qJm^k|G-Ek<`S5Br8%{7PTiemKnxuKjorf&q7B2Stbz%vjKf+ zzf7bzc{2GQGz*5-U_A4kpN&hBK*{ufF4-y=sz+L5TQNmEGF&7?RCnI`Kryvm9G_e=KGn~L*bFvM*Ib0y>ksW3!1f(T2!pVn zvrA17ER!+tV5`Zx^$hP#>{d!gj-;18+wVI3MYjuZx;q1QCY5wdG5{<+O?t@^chkGz zmuhsFsiINY2n6^;(Cj_}ReHUUPYp{Q`K3DAK^hA|*^LPKfnp4eg>>vj2!eE{PB*9p zJm6sjR`iud@U9y*mGRx!02)E48+9KzwF3X8FugcoU@RmPq){7BlD%J4sQ;2lWRsL-s9{Ga@|GwKL^@l##co%@@$GE(-x!LgXqi6H^?Y=i~Erxyq z8);azmOKU(oE&|ek2P<1Vu2>xNc{JzWoixtPrL)}MKri>uNZ;jQuBND0QRc)(WiQNazK%#u<%wR*D`hM zsRS#r1;4#vTP>Cw6~}(5>x_z;{z}+r@5O0V()Cdvj@elNQ!mpu0UuIb9`~QR7yWVk zcf)ikwuy_!71Z8C%S%$5)}MZ6&9y(fCxcUM z!}+?1{B5N>5cWw`p0(#@!3@tQ_jfmJB0GH%OD+c>|;<$?Ej$ ziPMr)x!mu{!^p_qYVPo6A}eBM9lW2S%S||Ro~(i89@(oY*G|AW8EfV8EGid=W%Yvc z%_v{Z$Im6xT}12G*yR_~@oEhGJ|ku`R{Axr1odyh9(zNuuWZgN(cN&l3!TN+<~jDG zuRfQT>s(x1*$9M$XyX;U$hNk4* zt%L^8ThMueFdVrI2oFskZOrgeY4OBQTQR3~>%4;7FM52f5_LQL9T?n4IgT~n24*OIL`K0xOHA?^VxE^VqSzS%#l?t=fVl93_p*uz;OFenzyh@r1wet-- zN5`Tw*PGr?n3sx5kxCOk!S79)YqdR$sWKy?dx?+EGCjHW(0ERF9i2&EW zJoz0O;0DlO-~`&4n#T!IL|Vyn#l4e>p7Uuwm;&&_8+UMUu>T2}@z0ivo$QIBQ_2zc zPNEFOt*O(84_4jXrsKYvZ{fAT_@A1M*&KTH2=5hr^`o;G<5??esUIXEoEI9|Cm24# zl`Hr#ivq;dFz0t$Ii+KHfKXN_+WyR|LqS!RnjRS|MfQlJc0kM`qh1?Bo&yI@wCjc|5=Zk9FSb5A*jx6?xDS(f6p=Hg8YcU zE)~MP3+0bkVL+_WHG4QPj+D7BmMSf0nEa%#`~sZ(p?dk%QZN}wiql^5SGqu08q|<3 zkp?MyhPzo*55>rFlgIh;`{?#I9jncP1khe?or(!l(vx1g?ye{IwARSxh4y%=Yj&3U zj#l0ntr_=aqvp*bf3ai0(&dO-YjQz`t{j{NliZy8;Ej45I}=@<*)~b0QQN8DMHFse zSfYwFVXL4r?X{%0d5*#19cps8+H^|+di5uwA{FXFmWlY)SL4WUM&q_b*No*5w{^dd zI)6LY2Bt~}lEQ#c-Apyd9PZ1*qYs3{UR?<6Z1*Ez*ixKv@p;8L{t?+lT!YQ^^V{P5 zlK$*OC!P@+ycW_ueAqb`9%;E($rUO(3v2jjqPYi#dD`z(we(WQwztPGvqyfOlwfPS z_c@a#?LL%s6hnJ-U|T)~Ewuk!|;ObYx~_ z)wA@HG@gi7*WB?|v+9Ur%KJ`WluM`mx&e-ySmaEypeJNWO5)vLP})~?eNEdX-GV(? z#lz`Y-SuQGV74cW(?4j{6|3$namMn+^Q8bS{(6@|TlW3G0_zI_05p@3lx!wHOQ2`2 zy>&VE&P3RZ6n9zG-ApRit4n7xZT%JV43=g*t^~gzq?+@Za?@rUzw8~RAHAKN9;5r2 z>Kcr!%YFTWJ+!m(XaHY|OSScJit zj(qQoGf)raPzju`Dnb7u#j6n0+Xbm za*e+VBgy1!i7{7Ywa0v-VC;wTV86rpr)Wn0Z+Pe7J9J2u6Xqj5Vvf<@*yiow+p+50 z}T5g9vR_~ z3-<)C!;4DR(dRSI=AZ0EOqnwwwmOM!-DOGUwzU9e>^C>|)50_qovlm;CQsH1gWdTB z5bmk$RXlR~D~v6zLoD-@g+D#Z+&0o>8y~S8lFjUW7s}TrmBqVh4(?6R>4=kV zbGRAxP(QtH?-Nju&pi}ZnNMk)kGRp5LqtXVwkjk0DJ*OC;!m%;xCiYb!)bwYQlFPqG8;g zys+fedN1$4jvqG=mVNVyg*$#B>2$9Wi_y7w%@{MDXv>EqpAsLPXz2X)@8@~1;Ju1? z5q72)uMHcH?ps-Lg1%O7{D)nO-gb99^Jq#iV`p!YjFr65V-cBcYxwnPf8pmAYZaXu zUwb1S_jhPH2Zo@pWnNX8>+ZQ)4a@B|pU+NAu*~dY#4AxFfr#Je%{%>!8#jillPuQc z7RMYMF%gETRqv$lPjT5vO0I$SHw)}7+D%3f7~w3(J}+=|3<_yAFl+JJ2@VKZnZIYB zO7#A^PYRHe7X`@Nsyiw(Pj5{^(BXRyYDIePRw}-Nq7pIBwN2x=@G$1hp+Nb&-Ah!K z(u7ySC>>nZ@)(Y?e|k}x2-g+<=FsR8J3}#7`ahtH3`bWgN1?u5Lf@N7Qq z_wtB?Gu53pGZK<3z+^Ih0LG%cyJ+W%W8L~jR{M$uIYl?FjsLab;39h0DzouP)#{Gv zH79RlU9qkzWx+*Sp0|E9c0Gsf%Y?0<&bgH6{d-i4QMN0oU3D^wBzS z5XOQ1=-sYW{>?Y7!Z))0Jf+v;ir8|mLFC9`h$v4ZIzeuF=dI@rQnf+S7D z`nuR@X}<;`V|a`?EWC~C)mBt}chj8(7|{3K9f4~U1ffCi{C1}6LEk)yEWzOhx~y`) z(c1VHm@eRl`P-=Ol`M}ueKg`;H!$MyPNO8D*1CQr8MZf}^Gf36C+tzAuZJa!5F6frSAwQ?B;sJ z(S<0hGIn)TCeS^(!)9w(+OyZIv*7nK|L{GL_kiy)$c&CFYq?)V-IRuF6#}BS_VREu zZ)LhI$g>;XDy3sqcRFY1YKn)ezS_gc@?rpx@6sw)j+?CE`EB2fcT?MsqQlJae*ct5s)@}jO{fJdsN?6TpHdCX_y}t@NMIRIq3Nu%U4jwx5OhHJIILWl zoI|f490u!Xsv()4RBe5<;|Ok=asSnUd*z-F&D*nIZ6??|EhgICuppjVzLd#cupghJnu3o}$ERRBiIaan^9y zFZ*MFXRn-BZ>0_{@1I>d!o3vqXM%gB0fN^GK&R-=oz4X5Ie#u%pM>V~aO0IjpO{=M zf~6>rIp3uuxD2N9oad2X)QBFrOtitYMvZT*U@XXr1jZm}lBcv_J{2+TZ`v5iIzc5%+^8t*h}}s(R(upJ;w`fOk#GcfmrTKZ9rNE&rmt za6Yy>Pv;n(31Ri=v)u`sXZT11niWm=UL0&cr4@C=aEhpQn zSM6{$4&%iV_Yp-w?nVu2Yi~ z{E?VwH0o`Sd1=tSmqkZ%{c!bS(NP>#Ckj9|z?{aEs)G6bv;14#tXzpQG16D#TEzd_!Gu+)XO+HzeOae|D5j0_eW zuVeg&)Jnj{r>-a#)YtlcBH`ot>Ms2bu(Q2nA8cJ_DleLQR81N{kN}C1)Q;GGExYB; z2)XCH^PTr2Op9Ls)T!~A4445c3iuzN@>qn!n8<0PQBi($Kss)13X&Q3t6zrXpanF! zs|PUe+gu8#Em5C3$G=QW9gXZj4g5rM-(@S%2a5E#h^CB;;sv(o_1CTek#fvF1^})7 z4}s%-3AYqR*#DXkWXk5&Q2$_Jnc;Pll-9$1j*o%$b(F&rOZwY`#z*X zeB{DdP-}`t3_4m}9f#ZC}LF`1r5qVGj%(>kXuTjBqea3kjYMEn`!hrywvf zy8qq(s_#{8^mxKkZ`xs`hmA9V&-B|cI2w6Wvia6Oa^|5kziRy&d?WjikALUPDRGsL z#cNtZ{vXR~(|%;i03`#{dEci%`{$h?opBaO<$>!5Hrw!@hk`-mz+}=NG=)5WAPoZ; z1g`I&(z*YlrG(J`U)_J~JG9$gd~TR&Q9!mAo2Ai_xi_{YXv$nPBPDtuc~&kV^(9RH z+3*9Lp1~orEL*=kHB-zCJThvUw(#(uyn)js`W@+}Q4JcQv8etl&E9v0#VsaU1-{k{ zVnN2F|JJ=|?6Iz+p3!gGMXcfXdun}KOva~(CN3-fh=goumep-hV!M$fYtH4wS$$)Y zMIWYEaWdc1-eZ>4HSrY5uCnT?(POqSXRo<=YN3Qbh#-H-5wMXSQ`m9mec{!(3 zTX@G8LCZnv(}_krED`DYt9zr@o8j;FPI`)?9;wuh_v(rs-iL&A{Q&mkb@%sTex*!& z=$ZUihq|Q~>zh)!&-eN0_zYvHok4Qo}L?jKW6Alvie!anWd*c{#?9 zemN$Ur`0ZccPr|>RxOpc${F6WNqogOJ(>cq(6eQ$qsn%j3h^yGtM3~yKNUO>cMX>f z_|9bt8pyx1KDNPz7Wv4=N?51lyOo2*ousCqH53SWln}6QulFybyAs-DyG}oP?j9s! z5QvH*rC-47u8Orgl7jGLWKNFOvI8O?5m3>*35^?7z$;vS^lP+qVmNKB+sLXqT1(@x z4ex11VP<6kqM2ZCu-^@OZsKXmoD8pBU{&P8EQ58PUhmRkMOEovva(k9PVP@tc2Ugi z@Ze(3Je8J9*qnHDLG0it3(>)Q`3vacm_j{88L6XV?{MMML&j3{5%`z1aWP)}_Ll+) zZr|e{S!|f(b*AKsWJ}-g@;!aKqaG9d-Y#@EkRcDt=tQ>)5i%VoGvZ*`<++!20_adrqjBq5#w{hPch118;&OM ze4c~;rh04hAuFr>4wL@Sk9raD!b4^S3S-LtPiZ^vmLJ-Ut?=D}l7$QnZ_V_!P79XtZHFyngolW3b$?HZzqjG3pyf_}$zG@NKeh;h#DKt*m zZfBsTHgPr1je=FpVqt0uMcC%oR?d~bynRvq*e}n`Y31@YyY>$}z+4MRZ-K8MLv@3Y zcQrqX%k=O`%CMo;zORsXr3P8RRCQ!Dez=YM#l?9=bjpp}99aPJz4H{{F`y6l?Kh^% zJ&&`ybKmBW;Z@Z@XBC!9*j<=P(VgG3t0{4)#-bx6o9QUVFf=84g!NK92$Wsa?Km@e z;n0fsZ@sgGowI3oH<+>=I>-~;N>)A(;a%{~n8MP-sua9P~+e<|q{#Qr6qmM{5WC@xxKJ-VI##J=IZtCPW-Y>OV0wDqu8oVNgGD<5H2XO%{z z_Z59QjXr}vF%~^PCSon3I@w_Ow^Km{PX5E$Y8tJppJg)6A-h%n`JOg4cxBsmD!m-* zC=eaK+qk78`%=x~gunXL@pd+_6o7yf4`(YV$j0N`E~=T%yO$KiJePPqCUlrN6}d=y zXqVfkXUYfsQDb;|k&%vGdGP#bLz47Ci%fo5MmYo2SqA5eIomuTIf$>b%(Q5iP}EkL z-TTp8FdM!-7WpDcx?u27g6MgXy;^7u3k z!;XFnprHXBF7=N6(!Rv@)N@1de?2k}PlP~5KFEQvzG+hU&U0y{v^x~|3Q+UL@wYKG z$5UOMS0|u-t$U5)@)2FerM`Q}*A3k!VfZMuSAL~=y4NFeE;d4BEZGj7^*Wiqtm??^ zzCF0NzC@|M*nd^~LGP_Og3I%&m{UutM_7p_u4`E}?9~e{cIHSGQ%q%2i-=$#3^D3V zXHTlW=2HVdJz+wMyu&gPQKZ8&3h`UyUTCOScE~Jn2@hX9rb&8;k-$xREh?}T<*H^% zl(<#>J*LjMcB1OLa4Icx#AYwRFjd*nI!Wf5Y{XVI8&RR>88|e4s`pti$q68+97zUJ)lOp`#F?S@oLiLf2Sm&&jcLT#4&l?bA<;%|Y!U-p92 z$@tEZpXi4jH*MKD8_YO2y&&^aC;6C{5wp}LN#!lDg@6FzFRCpb!@dlP__-Y3m2N#; z!1;#5nuJ--)^RSmHWzM5L;5XgI4&77r*gD5f%O@ef)3)6Y>W~3P<}6t+lFqj`K(@N z`@VYOrFjpgX zGNGQ8l4vWo*^`uu&H7>%QExvMzN-sh2ri4wiU)C4Rp!z`$D0;cF8`I7HU-U6o-JF_ zcVMW$)|luwnb#942dCeS{mdMx!&*$vM=m5J~I*0cw{NzT4jHv_l?+D7ZO7_PH?~c=QfpaZe`tLDKfc690 zsLv=9JdN=Uh?bp1N^~^4z8#XWWcfUjincAvka7f$ls%a;3O)XPB5(J^N3+u+s z<8*q;#2Kz2ShE0g`mH@`7oN=s`+5|*Q1+{UIVLN9_Pm4TP}JAAklhj2dBaNJMzb?Z zEj2V2eKOAN{BurRrLcM6ZGau|2cx6+cJm-oQ=bxp4X+?ibvqQTPjtx7cctV0P%;6c z4deA&^0p5t*8siG*#JN0jo1U@7{HJN00{xy7s?N;lQjw=KMeVFUgw_kdj2|ECW9#ws(;~q#_T^0E& z)Hk8M=XMc^7J7P{AQDa`<62ZpM#7PL@7_1ejvWpcU^2sA454(wC)E zGLM`3nKhh65PuFuJ;Nis$H3Dhu33V&FTakfo^&TEbTll#z4Y`r=v_GU?Xt=TPimm2 zqQURRNxmNwOn#h%iRSzHuS)m7j(KMZ+(`0u@Ze33#>dnFs>bjeN0M#vC6Z)i?OS~Y zq?-d;&jc-v=X(B+5CddYQy0Oc7o7=|oOv7B`;Ouq^ay4YV4dr=+v~wDe(JWQ2jyRy zyNckLT}c%4H`gr!o%DX!T$|qkcPVn4HqMeuxChx}0z*?r^5=_lllfrS%VnJ9kY?Dl z-r_@%Xpm(Kz)+j3=lvx4HEqW0xUAPy)tS`P+hKrf0y0P8G)Hz>ov}z)1uRhgP^SDq zGfdQ9M>2axEDK3e*b+ndhS)(47*p7vFcpaH?{pHdI#B=*fE8yQfXv9` zr`yUQ*nxkC*WIC2HJVxBw_o`D$(Q)GdNjwK9n5^iTWs*!NCjzM2i-rkjSI*K#vIf# z{O=Qfb0`W%f$Tyz8 z=eo1wyi!?rYldr%ew%+2gRGw@Zr788E8s^v^WS%r&x=t1ysLls|G3z=eo&<5-|fuz zWI{FnRwyE;|95?$*AwjVzu9!1`@#-?6O#Bs|FT#yMb+PyqWvFf-+xouO6|m_WcjU` zHn{%6ii`TaYgx`U{B^TC2j>$L)kS7PZx?x>5dQ$N)@#ncFQQ`K-eVzYAM_X&t4bEB z=K9iviyyx|7cw%+Xd7HO@1E}Oe^!P2_Hixcy@K{{`kS8rc=;UpoLzUfbUG$Jp6b}e z$^3ApIUSFuw({)dxDPqSE(w8`un5(M8=}82luK_jQM~hyNrm4H41QJ9V=D3|6TCjS zK0kk{si|3nDm1q6zqaceRJiSzGR%A2oRnj8Wp@T~&bFvnn%mPA)FwE`qZiurA=IWx z@20qYzOcPQxA5i(T&n+t>1;8MuH#fqR`JrGzSGME0#}ERz3cV^B*K-leZ6;Xd7BuN_}gVy!k1R#^F+W zn)A$U)d24kYlEXc1eYK*;L7UnOR!wV;S1Tz4Ye?7hf>>bz9;4`bMQS9f?Wz+RR^39 z-z%$ne--_4MM0Y5x=s2%%*SPJ_^#p7=9XJ!)^h=A08| zEZU?e$GXsVz*$3gl^1sNa34sy|LGnEr4U!bXrD{1JDah`cd_ zjN^MPRZCkoP^LpB)OX!qI7IWoYa-AKvrqh`iv55dV*~COLqWTHuQ4$tw8IvLUX8;# z+bHI^3w_X3?&HP?f78D_L}I+_-%arr>fwU>jQ6>_c##TY5#oEWa6L>9mEg}#i^m*)Vpp7FJl+<91Pmd$@z>R&ll10!(l3f>m|Am6~P&9#a-0hTIUi0(IK{QMl*Mp3?Z>UF)WfsuWGkAntDF z;DL)U!>!nmX28p7{fo_X4-TYUJGuCF0^?Ldb17gin3z6YWPD;OvNY)Vxa zJEu;F&U>S~B?0klHs-eQYUZ2L;c&hW&E?9|1ivy$$5(&-nm%o=8od;R=#sPW>@6?M zM+K2(?&UdW{Jgy9m_!Pt`B|c4Y?+>w#8K|B2`k~Pt(;lj`pzc+**6)MAhL{A4%(`vP9abDf|RH`Ra#c;Lb5{Ou&qv*vsPOd5?oY@t2 zfSTwtYmV0t8Z5IeB015}S}4~ z60okUPSCbpT>1Jk46C~*m*gy4AT#U8H#Cd~*_X&Du$1{qX=B0>tb9T9lJY!_5G8iJ z;d0q&STl-pcGd3es4duy-(vZ~a5V?#_1I3~-or`lR{Se004tMGgW}yRM;(mLcR zjB}9rlcpJ_KlHG{ok(EFABC3~Vy{?Q+s2tNu!urnqa7B*NuxWWlKpU`dhq%70CLxH z(xiJgcZG&klF>D5MRUY#YnyfX-RMF z09O7POe2C)jf`=Uu>7KzZ?6d1t;h(4Jes1)E1Sc7tZyNCr!$X2Vcc%ua4sS_K`TmN zZR1OFf=dV=B_-=G)5we&#?uy00hRrMsj4Vxt;6m^V4MD0@hRE~@1XovdloM7F+6WI z5?*;K^nGZ-YTjC4X|b#-NyV;8N3I%CJSLr0KK}WNhN?~TnE9~()7eQ70iT2e&J#aF zcI%*#^F#8sJS3#i*(8rSTFQe~s{10Fbhvyf7cw7qCcBOku-caH7rR>rY-WDujSYCq z7K2#Yf9~kn3qOV(6)AV!=s!_DAS=1vVtOZJa(pPm_s~UVd_=`Ystktw)Y`%r`iINp zeHy#!z1kcvpPk@0I%XU%{Y>vF6H7Xz#IE5ojM(Q?s&W~iNqvAsHvV0}GF|gN*_I7h z5qr`6uG!h&iowMu=plV4KX3Kb=R{Szr5v$ej5ntn66cqi=dP}1_V6R}7R&G&^JnJ2 zH)HCqPNef|_bG081Kt)B8e>5h4(rF4M!n+$Dv%A)e)lLu{g%8ki>;x}h4+)$yGqMf zk?N;6wcs zQ`a*F{o8`$Oz?!pKX^`*@4b1lsiv%2U^+3~MI&2nwM4{l;x8mByaim=l{uGBqjGU+ zIP$w*x$lRRK^(Rjdz-h|i@$z_*Vz8-Tr_ziy0`VM?gW!`qQa!>XA@aFGktu24w?=b zvAi0qLhuQ<+EM-Zkk<5CuSDs65f`YuUUOl32if8M&>Ung2iPsrV?DB9Bk=Cmrz*;D zjATCYn;Wew2a@9yw3-$%tJLKZV`$tCs9DTI2_lFgL4*iN z-qE|d*1K!H_163A?6c0^=j?BP-}%1Z{?$kZ+}G7Q~Al}AbM7O=+_rbK8sh7tcW{po?$h^V7RmgV7XB}U(M zT9Y$Ogugs0Eqa1K;7=c|V(Fu%JODv2BKn8>)H*V?r>*y^hQHCj2pZ!RWipC==Q3!> zO}S%8cfn~p;=lAc4&4(3k%#xW3}Hf6ButF`uSo- z-rFF+aHrlx1IZ*{KUeoEpSJZ?JuiD{ac@}DmyX*pc)=Z7Q8|A$F2rTLbG%R&Dk#2f z15Z#?f!=~jOO1Hho|ofj7{o-+b`E|8;t62^yZ!iI>dBHdw{HbX0&-@URC^DU zB<@dGTs4!2it^jk>V9i`bBPgNMl_!YudDV)-&$n7e0hA>{GrG!<&l*bq!8{yfUl6Y z)0Bondp@r?nE!z~r`1^2fkf@HA^85J*DGJzD2gc@(lzo9A$e*z>+8qz+KJ#;W@Fk# zHJ6}Lu2Az{BrkyuUXOGdAAN5T?vssydp)BErUzf-R1|YIBy6wEZFW@-uJkXs(APRn z`1MagO=#=bpNzWkPf;eRGADIp7(o2{TcX!vH#Q}P(>dtgn*`Yz$6M;&1I(;CK%>!z zvB`drR1>sjoBG5%0PUxe!>Jlcc72QwLozZz_!f2T_+n*anIvVhL=lb611z%r>#3sf z(SGumg;i-~a#k||J{&Z!dE`>;g2tP4W>$N_>&)kUYS6r=b-QWn={iP1Fx@Y1Rm&8% zuA-Bd{gEnfm`GuL4}H7Bb&o{`*`o`Qm{l<`vnBjvy^TfinXgqm-=9;Ylvb^M;qoc7 z{`lO=%fs4YdOElt%u_Wp;Y$X~hV}zbO5hQ<1@SP%dsUU}0=FX;&rfQ($}0jb>dbb~ zt6cdA%v-Al1>l}Kt9w#HurjGok6!-01Ag8?ANbKh6?lV)P_`wxhP$3Bts-Ps4lBpy zNY8ps=W)n@&oJV{@9(=3M_(1gWvJUL4d}FW4253 zQ$LrLjnI*sUVsjN(vHS*pL{N?sF_BsILd{}qURqAx5t@ADGebrswrke+T}t^!K7&{c&!d+W5=*U%>D4AoA3q?FK$Ol$>E zhchiUYgi!oG{nstI$F2nV7~N_odJMd*e$_ZRfdz(#}ed==#jOl70qA@W(Ss;J$jJl z*DR-tj(b?-=+6tQdtH%uf9>Yi5R{Qo=*Qo~sSkUVI}gbjaSglZnDi9&5^{*p8G5aH zQFlS%Ci%?W^hzk&)AU+?ur3$+Y!Ty65og~9w zJ8s;9C{2&pxyJwr@>vTBOss(Q7%GQ5S#mreNL1^S2oz$Gs%bo=gZoXhI!v7lu{De2 zV}^)Or6Yht$SAE1Z3Z@BFGS0ur=*DRB_@A%Rb?v0G6}=TOc{M{Sk)ULD3HRxf>3)_ zwv$H+1VZuP%H@_Ti1$|O)4W4ah%{;o+_N+7ZO~%)oe6yIOn*hmO-MY@pczqxI=2pF zTrc0fCnF49m8xCnb}vUf<4$&(r@deM8?D#nac-B$)A_a0UmcdLJ9^YR)S`p0jZckU z=^j~>ihq+CS-l~3yQ6%v&*O9?Z@?Y!wrR1f)>vvsyPTvB_p$Eds*+15V=8$VENiY7 zZ}c-aNIh1P`WP*jYA=rK43T%MaNgHP3+n$~Q1t->~`vdig zht{Qs*$W6A898XvuDiXSyJxyMLO3*WUx9kL9Vo~AIw@}n!1oet!&0*52H5LRx;%Fb zQA;z34}l5K1pweXti1qC=v=u=LQ9XAuEzK$7(uRrhP!a}QA*B75E0?@2WJgj*WARrk89|D?OHtk-bHl>4XA*sEiaf$=(~93;BMeCqRvsW#4s85S1MB<5P9%7AKt8aj>e5_hwvud+7ou~_Lxn-x=WHcD^!@7c^m z@Yb&^wk;mb{Xk_k3|0v5Eq~(i1NF$;zUQN*Vkc7WG6o14@%HM`vtuz7kdRncH#*LB zrwWyXx#%s<#@v{Odu78gY9QA!?L`EW2?oi!{>kke8?_a;WluYTPR-7xCWCqO$xnmx z5|EA1HK86gnzj_vi=chNqZhfy()z{T*B6t9N=0)?wG-7#vpOgsOz>n+(b%Z+Xb%ea! zo;l^TgU0tCQGVFv7O=+cT30ssvD;Z)=C|GXKt15IAG7~x_Fqot%ioq1Qb07-`+i!c zGS=72C9P-r5h^Av)+#vr(g!F@*W*CCY%YBT`}yhMBh1^S)Pn(JfKbmO~_xd|uDJeLk{Ot$3yIP9?}frt^7< zD@;*asE#}6q6Hjh8We6eBWWvV4Se%HR(F*jy=)m(RXRPl@5;G*yIi!Yx}fLp?K(h3 z1?D9!K$Huiy;lk`lz{p%RM-LDU!d0IlZqCFCsQ`Nz88|OeUJpI&ak7(O6$iW>-cj= z*Y1a6CrzxAu9=)=pxMr*}fP83gfuA_(3D5`T3vnnErx3t*!`zflp zO+^2p0X+iqopLH-)J|7cJl6x!)5)VyR(>seB5qo$Zy}?COTh^bVz0KegU|c_Nxp$} zA0u-CxkpCM-JKeB1Ybt==|IB{^O``d#x2kOWq$2SiRqoVe*^7ygP8yT diff --git a/Apps/MidiController/images/14_baby8_mode.png b/Apps/MidiController/images/14_baby8_mode.png deleted file mode 100644 index 1b6f8ae9cc961cf69ba4d6a95c2acfb0400b5c8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38557 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

KfP=v-DeoPS#b0ftk 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 G;r{_zpKt*H diff --git a/Apps/MidiController/images/1_mode_selection.png b/Apps/MidiController/images/1_mode_selection.png deleted file mode 100644 index 8802d1e42bb8e6107da79c3c4a6b6401183c4ddf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53507 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 diff --git a/Apps/MidiController/images/3_beats_mode.png b/Apps/MidiController/images/3_beats_mode.png deleted file mode 100644 index 4728533bd4644dc09e839639116d22cee2895550..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31370 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 diff --git a/Apps/MidiController/images/5_drop_mode.png b/Apps/MidiController/images/5_drop_mode.png deleted file mode 100644 index 31c9137d84616ff9cf1ec238524a701ef9e50a78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32245 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 diff --git a/Apps/MidiController/images/8_arp_mode.png b/Apps/MidiController/images/8_arp_mode.png deleted file mode 100644 index 731c295ae5d287dc0e3ea35efb93737a395ca76f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43456 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 VM87cR|M#xEYga9dDh-`t{{shZFQ5Pb diff --git a/Apps/MidiController/images/9_grid_mode.png b/Apps/MidiController/images/9_grid_mode.png deleted file mode 100644 index 08ca042d70bfea1287c62a875102edf00811178c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23991 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 yQJ(Aq*(=}oKPIA Date: Wed, 28 Oct 2026 21:50:35 +1000 Subject: [PATCH 3/5] versions --- Apps/Doom/manifest.properties | 4 ++-- Apps/GraphicsDemo/manifest.properties | 4 ++-- Apps/MystifyDemo/manifest.properties | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Apps/Doom/manifest.properties b/Apps/Doom/manifest.properties index d09b667..5131bb0 100644 --- a/Apps/Doom/manifest.properties +++ b/Apps/Doom/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32p4 id=tactility.doom -version.name=0.12.0 -version.code=12 +version.name=0.13.0 +version.code=13 name=Doom description=Classic DOOM. Tab5 only, keyboard required to play. app.0.id=tactility.doom diff --git a/Apps/GraphicsDemo/manifest.properties b/Apps/GraphicsDemo/manifest.properties index bc23255..16c8bd1 100644 --- a/Apps/GraphicsDemo/manifest.properties +++ b/Apps/GraphicsDemo/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.graphicsdemo -version.name=0.19.0 -version.code=19 +version.name=0.20.0 +version.code=20 name=Graphics Demo app.0.id=tactility.graphicsdemo app.0.name=Graphics Demo diff --git a/Apps/MystifyDemo/manifest.properties b/Apps/MystifyDemo/manifest.properties index b294438..57ee46f 100644 --- a/Apps/MystifyDemo/manifest.properties +++ b/Apps/MystifyDemo/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.mystifydemo -version.name=0.20.0 -version.code=20 +version.name=0.21.0 +version.code=21 name=Mystify Demo description=A graphics demo that is inspired by a Windows 95 screensaver. app.0.id=tactility.mystifydemo From a85fd9c1c26165a13bccaf068a5554c2916d3dca Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 29 Oct 2026 09:54:24 +1000 Subject: [PATCH 4/5] updates --- Apps/MidiController/main/Source/MidiController.h | 2 +- Apps/MystifySpiDemo/README.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Apps/MidiController/main/Source/MidiController.h b/Apps/MidiController/main/Source/MidiController.h index b4ae99d..e0b3c39 100644 --- a/Apps/MidiController/main/Source/MidiController.h +++ b/Apps/MidiController/main/Source/MidiController.h @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include diff --git a/Apps/MystifySpiDemo/README.md b/Apps/MystifySpiDemo/README.md index 1643773..fada577 100644 --- a/Apps/MystifySpiDemo/README.md +++ b/Apps/MystifySpiDemo/README.md @@ -3,6 +3,10 @@ An app package that adds a 2.8" ILI9341 SPI panel to Tactility and renders a Mystify scene on it (and mirrors it on the primary display). +Display used in this example: 2.8TFT- ILI9341 IC +https://www.aliexpress.com/item/1005006323678902.html +https://www.lcdwiki.com/2.8inch_SPI_Module_ILI9341_SKU%3AMSP2807 + The package contains five binaries: | Binary | Role | @@ -22,7 +26,7 @@ it starts; opening the demo stops LVGL, takes over the panel through `gfx::SinkT | Panel | Tab5 M-BUS | Note | |-------|------------|------| -| VCC | Ext5V | The module has its own 3.3 V regulator | +| VCC | Ext5V | The module has its own 3.3 V regulator or bridge J1 and use 3.3V instead of Ext5V | | GND | GND | | | CS | GPIO51 | | | RESET | GPIO4 | | From 2b76c864e6562206537b5ba47a63c08cb5710691 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 29 Oct 2026 15:00:20 +1000 Subject: [PATCH 5/5] update mystifyspidemo --- Apps/MystifySpiDemo/README.md | 57 +++- .../main/Source/Esp32SpiConfig.h | 37 +++ .../main/Source/Ili9341Spi.cpp | 253 +++++++++--------- .../main/Source/Ili9341Spi.h | 15 +- .../main/Source/Panel.cpp | 97 ++++--- Apps/MystifySpiDemo/manifest.properties | 5 +- .../main/Source/Application.cpp | 225 ++++++++++++++-- Libraries/Gfx/Source/Polygon.cpp | 13 +- 8 files changed, 497 insertions(+), 205 deletions(-) create mode 100644 Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Esp32SpiConfig.h diff --git a/Apps/MystifySpiDemo/README.md b/Apps/MystifySpiDemo/README.md index fada577..b0764e3 100644 --- a/Apps/MystifySpiDemo/README.md +++ b/Apps/MystifySpiDemo/README.md @@ -42,17 +42,30 @@ it starts; opening the demo stops LVGL, takes over the panel through `gfx::SinkT 1. Registers the `ili9341-spi` driver (`driver_construct_add`). 2. Loads `ili9341panel.properties` from the package's user data directory (defaults on first run). -3. Calls `spi_bus_initialize()` for the configured host/pins and creates the display device. +3. Creates an `espressif,esp32-spi` controller device at runtime from the saved host and pins, then + creates the `ili9341-panel` display device as its child. 4. LVGL picks up the device when it starts and binds it as a second `lv_display_t`. -On stop it removes the device and frees the bus. If the panel fails to start, the module still -stays loaded so the configuration can be fixed and re-applied. +On stop it removes the display and the controller and releases the pins. If the panel fails to +start, the module still stays loaded so the configuration can be fixed and re-applied. The driver itself (`Ili9341Spi.h`/`Ili9341Spi.cpp`, including the DisplayApi and MADCTL handling) lives in the module too, next to `Panel.cpp`/`Settings.cpp`. It used to be the standalone `Libraries/Ili9341Spi`; since the module is the only consumer and app-packaged drivers are proven, keeping it there makes the package self-contained. +The driver talks to the kernel SPI controller API (`spi_controller_transmit()`), not the raw IDF +`spi_master` calls: + +- One half-duplex peripheral per panel: writes run at full clock with no dummy phase, which is what + keeps the display stable at 80 MHz. +- CS and DC (and RESET on the way in) are owned by the driver as `gpio_descriptor`s, so the + controller never drives a CS line and no bus acquire/release dance is needed. +- Frames live in PSRAM and are handed to the controller as-is. `esp32_spi` sets + `SPI_TRANS_DMA_USE_PSRAM` for external transmit buffers, so there is no bounce buffer and no + copy through internal RAM. +- Register reads (MADCTL) go through the controller's half-duplex receive path. + ### Settings API Exported by the module and resolved by apps at runtime (`module_resolve_symbol_global_get`): @@ -136,13 +149,30 @@ display, calls `ili9341panel_apply()` and re-binds it. - Resolves `ili9341panel_get_device` to hold a module reference and get the panel device. - Stops LVGL (done by the entry point), then creates `ExternalFirstDisplaySink` + `gfx::SinkTarget` at the device's own resolution. The sink subclass prefers PSRAM because - `gfx::DisplaySink`'s default policy requires internal DMA memory; the driver sets - `SPI_TRANS_DMA_USE_PSRAM` so the SPI engine reads directly from PSRAM. + `gfx::DisplaySink`'s default policy requires internal DMA memory; `esp32_spi` sets + `SPI_TRANS_DMA_USE_PSRAM` for external transmit buffers, so the SPI engine reads directly from + PSRAM. - Mirrors the same normalised simulation on the primary display via `gfx::Target`, using `set_transform()` on portrait-native panels (Tab5) with a 640x360 logical frame that the PPA scales/rotates up. - Clears both panels and restarts LVGL on exit. +### Frame budget + +The panel is the slow path: a 320x240 RGB565 frame is 153.6 KB and takes ~15 ms of SPI time at +80 MHz, plus the PSRAM cache writeback the sink does before presenting. + +- The panel is **not** cleared every frame. Each walker keeps one extra trail snapshot, so the + previous frame's echoes (now at ages 1..TRAIL_LENGTH) are erased in black, together with the two + text bands; only the touched regions are presented. +- `Surface::drawPolygon()` marks each edge's bounds instead of the whole quad, so outline-only + scenes don't dirty their entire bounding box. +- The primary mirror runs on its own FreeRTOS task (pinned to the other core, below the app's + priority) at `PRIMARY_FPS`. A mirrored frame is a 640x360 draw plus a PPA scale and a 720x1280 + present (~29 ms); keeping it out of the panel loop is what lets the panel stay at 30 fps. +- The panel loop sleeps out the rest of the `TARGET_FPS` budget, so a fast frame doesn't spin the + CPU (which would starve the mirror task). + ## Build and install ```sh @@ -166,9 +196,14 @@ built before those were added makes those apps fail to load; rebuild and flash i module logs the driver's reason; check the wiring, then save/apply settings. - **White screen with correct wiring** — toggle `invert_color` (`true` gives a negative image on this module), or lower `pixel_clock_hz` to 10-20 MHz. -- **App fails to load with a missing `spi_*` or `lv_display_get_next`/`lv_display_get_driver_data` - symbol** — the firmware predates those exports in `Platforms/platform-esp32/source/module.cpp` - and `Modules/lvgl-module/source/symbols.c`; rebuild and flash it, then re-run - `Scripts/check-app-symbols.py`. -- The unit's controller doesn't implement RDDID (it returns zeros) but does answer register reads; - the driver logs `Panel responds; RDDID is not implemented by this controller` and continues. +- **App fails to load with a missing `spi_controller_*`, `gpio_descriptor_*` or + `lv_display_get_next`/`lv_display_get_driver_data` symbol** — the firmware predates those exports + in `Platforms/platform-esp32/source/module.cpp` and `Modules/lvgl-module/source/symbols.c`; + rebuild and flash it, then re-run `Scripts/check-app-symbols.py`. +- **Module fails to load after a firmware update** — `struct SpiDeviceConfig` gained + `half_duplex`, which changes the kernel API/ABI. Flash a firmware built after that field was + added (and regenerate the distribution SDK from it). +- The unit's controller doesn't implement RDDID (it returns zeros) but does answer register reads + at 10-20 MHz; the driver logs `Panel responds; RDDID is not implemented by this controller` and + continues. At 40-80 MHz the MADCTL readback can return a shifted value (`3C`, `1E`) even though + the panel is fine; treat the readback as a diagnostic, not a failure, above 20 MHz. diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Esp32SpiConfig.h b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Esp32SpiConfig.h new file mode 100644 index 0000000..19132a2 --- /dev/null +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Esp32SpiConfig.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +/** + * Mirror of `platform-esp32`'s `tactility/drivers/esp32_spi.h`. + * + * The platform's header is not shipped in the app SDK, but an app-packaged module can still create + * an esp32 SPI controller device by passing a config with this exact layout to + * device_construct_add_start(..., "espressif,esp32-spi"). Keep it in sync with the platform header. + */ + +#include +#include + +#include + +struct Esp32SpiConfig { + spi_host_device_t host; + /** Clock pin */ + struct GpioPinSpec pin_sclk; + /** Data 0 pin */ + struct GpioPinSpec pin_mosi; + /** Data 1 pin */ + struct GpioPinSpec pin_miso; + /** Data 2 pin */ + struct GpioPinSpec pin_wp; + /** Data 3 pin */ + struct GpioPinSpec pin_hd; + /** Data transfer size limit in bytes. 0 means the platform decides the limit. */ + int max_transfer_size; + /** Array of chip select GPIO pin specs */ + struct GpioPinSpec* cs_gpios; + /** The item count of cs_gpios */ + uint8_t cs_gpios_count; + /** Weak internal pull-up on MISO. */ + bool miso_pull_up; +}; diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.cpp b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.cpp index 2885ef9..8cc08cd 100644 --- a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.cpp +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.cpp @@ -1,19 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 #include "Ili9341Spi.h" -#include -#include #include #include #include #include #include +#include +#include +#include +#include #include #include #include -#include #include constexpr auto* TAG = "Ili9341Spi"; @@ -76,9 +77,11 @@ constexpr InitCommand INIT_COMMANDS[] = { }; struct State { - spi_device_handle_t device = nullptr; - int pin_dc = -1; - int pin_cs = -1; + struct Device* controller = nullptr; + struct SpiDevice* device = nullptr; + GpioDescriptor* dc = nullptr; + GpioDescriptor* cs = nullptr; + GpioDescriptor* reset = nullptr; uint8_t madctl = 0; int32_t gap_x = 0; int32_t gap_y = 0; @@ -89,6 +92,21 @@ State* getState(Device* device) { return static_cast(device_get_driver_data(device)); } +void releasePins(State* state) { + if (state->dc != nullptr) { + gpio_descriptor_release(state->dc); + state->dc = nullptr; + } + if (state->cs != nullptr) { + gpio_descriptor_release(state->cs); + state->cs = nullptr; + } + if (state->reset != nullptr) { + gpio_descriptor_release(state->reset); + state->reset = nullptr; + } +} + const Ili9341SpiConfig* getConfig(Device* device) { return static_cast(device->config); } @@ -98,36 +116,23 @@ const Ili9341SpiConfig* getConfig(Device* device) { * continues the same panel command (command byte -> parameters, or RAMWR -> pixel data). */ error_t transmit(State* state, const void* data, size_t size, bool keep_cs, bool dc_high) { - if (state->device == nullptr || data == nullptr || size == 0 || size > state->max_transfer_bytes) { + if (state->controller == nullptr || state->device == nullptr || + data == nullptr || size == 0 || size > state->max_transfer_bytes) { return ERROR_INVALID_ARGUMENT; } - gpio_set_level(static_cast(state->pin_dc), dc_high ? 1 : 0); - if (state->pin_cs >= 0) { - gpio_set_level(static_cast(state->pin_cs), 0); + gpio_descriptor_set_level(state->dc, dc_high); + if (state->cs != nullptr) { + gpio_descriptor_set_level(state->cs, false); } - spi_transaction_t transaction = {}; - transaction.length = size * 8; - if (size <= sizeof(transaction.tx_data)) { - // Short transfers avoid a DMA bounce buffer entirely. - transaction.flags |= SPI_TRANS_USE_TXDATA; - std::memcpy(transaction.tx_data, data, size); - } else { - transaction.tx_buffer = data; + // The address may be PSRAM (Gfx frame buffers); the platform controller sets + // SPI_TRANS_DMA_USE_PSRAM for external buffers, so no bounce copy is needed. + const error_t result = spi_controller_transmit(state->controller, state->device, data, nullptr, size); + if (state->cs != nullptr && !keep_cs) { + gpio_descriptor_set_level(state->cs, true); } -#if SOC_PSRAM_DMA_CAPABLE - // A Gfx frame buffer usually lives in PSRAM. Ask the SPI DMA engine to read it directly - // instead of bouncing every chunk through a temporary internal buffer. The flag is harmless - // for buffers that turn out to be internal; the driver checks the address itself. - transaction.flags |= SPI_TRANS_DMA_USE_PSRAM; -#endif - - const esp_err_t result = spi_device_polling_transmit(state->device, &transaction); - if (state->pin_cs >= 0 && !keep_cs) { - gpio_set_level(static_cast(state->pin_cs), 1); - } - return result == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; + return result; } /** Command byte, followed by its parameters (both with CS held across the boundary). */ @@ -162,35 +167,33 @@ error_t writePixels(State* state, const void* data, size_t size) { /** Reads as many bytes as fit in `size`, discarding any extra bytes the panel returns. */ error_t readData(State* state, uint8_t* buffer, size_t size) { - spi_transaction_t transaction = {}; - transaction.rxlength = size * 8; - transaction.rx_buffer = buffer; - transaction.length = 0; + if (state->controller == nullptr || state->device == nullptr) { + return ERROR_INVALID_STATE; + } - gpio_set_level(static_cast(state->pin_dc), 1); - if (state->pin_cs >= 0) { - gpio_set_level(static_cast(state->pin_cs), 0); + gpio_descriptor_set_level(state->dc, true); + if (state->cs != nullptr) { + gpio_descriptor_set_level(state->cs, false); } - const esp_err_t result = spi_device_polling_transmit(state->device, &transaction); - if (state->pin_cs >= 0) { - gpio_set_level(static_cast(state->pin_cs), 1); + const error_t result = spi_controller_transmit(state->controller, state->device, nullptr, buffer, size); + if (state->cs != nullptr) { + gpio_descriptor_set_level(state->cs, true); } - return result == ESP_OK ? ERROR_NONE : ERROR_RESOURCE; + return result; } error_t applyMadctl(State* state) { return writeCommand(state, CMD_MADCTL, &state->madctl, 1); } -void pulseReset(const Ili9341SpiConfig* config) { - if (config->pin_reset < 0) { +void pulseReset(State* state, const Ili9341SpiConfig* config) { + if (state->reset == nullptr) { return; } - const auto pin = static_cast(config->pin_reset); - const int active = config->reset_active_high ? 1 : 0; - gpio_set_level(pin, active); + const bool active = config->reset_active_high; + gpio_descriptor_set_level(state->reset, active); vTaskDelay(pdMS_TO_TICKS(10)); - gpio_set_level(pin, !active); + gpio_descriptor_set_level(state->reset, !active); // The ILI9341 needs 120 ms after a hardware reset before it accepts SLPOUT. vTaskDelay(pdMS_TO_TICKS(120)); } @@ -198,15 +201,8 @@ void pulseReset(const Ili9341SpiConfig* config) { error_t initialisePanel(State* state, const Ili9341SpiConfig* config) { error_t error = ERROR_NONE; - if (config->pin_reset >= 0) { - gpio_config_t reset_config = {}; - reset_config.mode = GPIO_MODE_OUTPUT; - reset_config.pin_bit_mask = 1ULL << config->pin_reset; - if (gpio_config(&reset_config) != ESP_OK) { - LOG_E(TAG, "Failed to configure the reset pin"); - return ERROR_RESOURCE; - } - pulseReset(config); + if (state->reset != nullptr) { + pulseReset(state, config); } // Always send a software reset as well: it is harmless after a hardware reset and covers a @@ -287,51 +283,45 @@ bool readPanelId(State* state) { * adapter's control wires can be identified at the panel connector with a multimeter. Ends with * RESET and CS released (high), then the caller re-initialises the panel. */ -void probeControlPins(const Ili9341SpiConfig* config) { +void probeControlPins(State* state, const Ili9341SpiConfig* config) { struct Pin { const char* name; + GpioDescriptor* descriptor; int pin; }; const Pin pins[] = { - { "DC", config->pin_dc }, - { "RESET", config->pin_reset }, - { "CS", config->pin_cs }, + { "DC", state->dc, config->pin_dc }, + { "RESET", state->reset, config->pin_reset }, + { "CS", state->cs, config->pin_cs }, }; LOG_W(TAG, "Panel didn't answer: identifying control pins (3 s each, measure at the panel)"); for (const auto& probe : pins) { - if (probe.pin < 0) { - continue; + if (probe.descriptor != nullptr) { + gpio_descriptor_set_level(probe.descriptor, false); } - gpio_config_t output = {}; - output.mode = GPIO_MODE_OUTPUT; - output.pin_bit_mask = 1ULL << probe.pin; - if (gpio_config(&output) != ESP_OK) { - continue; - } - gpio_set_level(static_cast(probe.pin), 0); } for (const auto& probe : pins) { - if (probe.pin < 0) { + if (probe.descriptor == nullptr) { continue; } LOG_I(TAG, "%s (GPIO%d) HIGH for 3 s", probe.name, probe.pin); - gpio_set_level(static_cast(probe.pin), 1); + gpio_descriptor_set_level(probe.descriptor, true); vTaskDelay(pdMS_TO_TICKS(3000)); - gpio_set_level(static_cast(probe.pin), 0); + gpio_descriptor_set_level(probe.descriptor, false); vTaskDelay(pdMS_TO_TICKS(500)); } // Leave the control lines in their idle state. - if (config->pin_reset >= 0) { - gpio_set_level(static_cast(config->pin_reset), 1); + if (state->reset != nullptr) { + gpio_descriptor_set_level(state->reset, true); } - if (config->pin_cs >= 0) { - gpio_set_level(static_cast(config->pin_cs), 1); + if (state->cs != nullptr) { + gpio_descriptor_set_level(state->cs, true); } - if (config->pin_dc >= 0) { - gpio_set_level(static_cast(config->pin_dc), 0); + if (state->dc != nullptr) { + gpio_descriptor_set_level(state->dc, false); } } @@ -346,73 +336,93 @@ error_t start(Device* device) { if (state == nullptr) { return ERROR_OUT_OF_MEMORY; } - state->pin_dc = config->pin_dc; - state->pin_cs = config->pin_cs; state->max_transfer_bytes = config->max_transfer_bytes; state->madctl = (config->swap_xy ? MADCTL_MV : 0) | (config->mirror_x ? MADCTL_MX : 0) | (config->mirror_y ? MADCTL_MY : 0) | (config->bgr_order ? MADCTL_BGR : 0); - gpio_config_t dc_config = {}; - dc_config.mode = GPIO_MODE_OUTPUT; - dc_config.pin_bit_mask = 1ULL << config->pin_dc; - if (gpio_config(&dc_config) != ESP_OK) { - LOG_E(TAG, "Failed to configure the DC pin"); + struct Device* gpio0 = nullptr; + if (device_get_by_name("gpio0", &gpio0) != ERROR_NONE || gpio0 == nullptr) { + LOG_E(TAG, "The native GPIO controller (gpio0) was not found"); + delete state; + return ERROR_NOT_FOUND; + } + + state->dc = gpio_descriptor_acquire(gpio0, config->pin_dc, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO); + if (state->dc == nullptr) { + LOG_E(TAG, "Failed to acquire the DC pin"); delete state; return ERROR_RESOURCE; } - gpio_set_level(static_cast(config->pin_dc), 0); + gpio_descriptor_set_level(state->dc, false); // Chip select is driven as a plain GPIO rather than by the SPI peripheral. That keeps the - // whole panel interface under our control (and survives the bring-up probe re-configuring the - // pin), which also makes a stubborn panel easier to diagnose. - gpio_config_t cs_config = {}; - cs_config.mode = GPIO_MODE_OUTPUT; - cs_config.pin_bit_mask = 1ULL << config->pin_cs; - if (gpio_config(&cs_config) != ESP_OK) { - LOG_E(TAG, "Failed to configure the CS pin"); + // whole panel interface under our control (and survives the bring-up probe), which also makes + // a stubborn panel easier to diagnose. + state->cs = gpio_descriptor_acquire(gpio0, config->pin_cs, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO); + if (state->cs == nullptr) { + LOG_E(TAG, "Failed to acquire the CS pin"); + releasePins(state); delete state; return ERROR_RESOURCE; } - gpio_set_level(static_cast(config->pin_cs), 1); - - spi_device_interface_config_t device_config = {}; - device_config.mode = 0; - device_config.clock_speed_hz = static_cast(config->pixel_clock_hz); - // -1: CS is manual (see transmit()), so the SPI peripheral must not drive it. - device_config.spics_io_num = -1; - device_config.queue_size = 1; // polling transactions only - // Half duplex allows receive-only transactions (the panel ID read); full duplex rejects - // rxlength > length, which a write-only display otherwise never needs. - device_config.flags = SPI_DEVICE_HALFDUPLEX; - esp_err_t result = spi_bus_add_device( - static_cast(config->spi_host), - &device_config, - &state->device - ); - if (result != ESP_OK) { - LOG_E(TAG, "Failed to add the SPI device: %s", esp_err_to_name(result)); + gpio_descriptor_set_level(state->cs, true); + + if (config->pin_reset >= 0) { + state->reset = gpio_descriptor_acquire(gpio0, config->pin_reset, GPIO_FLAG_DIRECTION_OUTPUT, GPIO_OWNER_GPIO); + if (state->reset == nullptr) { + LOG_E(TAG, "Failed to acquire the reset pin"); + releasePins(state); + delete state; + return ERROR_RESOURCE; + } + gpio_descriptor_set_level(state->reset, true); + } + + struct Device* parent = device_get_parent(device); + if (parent == nullptr || device_get_type(parent) != &SPI_CONTROLLER_TYPE) { + LOG_E(TAG, "The panel device is not a child of an SPI controller"); + releasePins(state); delete state; - return ERROR_RESOURCE; + return ERROR_INVALID_STATE; + } + state->controller = parent; + + const SpiDeviceConfig spi_config = { + .clock_speed_hz = config->pixel_clock_hz, + .mode = 0, + // CS is a plain GPIO driven by transmit(), so the controller must not drive one. + .cs_index = SPI_CS_NONE, + // The panel is write-mostly; half-duplex also lets the platform use the same timing + // calculation the old direct SPI device had at 80 MHz. + .half_duplex = true, + }; + error_t error = spi_controller_add_device(parent, &spi_config, &state->device); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to add the SPI device: %s", error_to_string(error)); + releasePins(state); + delete state; + return error; } // Keep the bus for this panel: command and pixel transactions rely on CS staying asserted // between them, so no other device may interleave. - spi_device_acquire_bus(state->device, portMAX_DELAY); + spi_controller_acquire_bus(parent, state->device); - error_t error = initialisePanel(state, config); + error = initialisePanel(state, config); if (error == ERROR_NONE && config->log_panel_id) { if (!readPanelId(state) && config->probe_control_pins_on_id_failure) { - probeControlPins(config); + probeControlPins(state, config); error = initialisePanel(state, config); } } if (error != ERROR_NONE) { LOG_E(TAG, "Panel initialisation failed"); - spi_device_release_bus(state->device); - spi_bus_remove_device(state->device); + spi_controller_release_bus(state->controller, state->device); + spi_controller_remove_device(state->controller, state->device); + releasePins(state); delete state; return error; } @@ -434,8 +444,9 @@ error_t stop(Device* device) { return ERROR_NONE; } - spi_device_release_bus(state->device); - spi_bus_remove_device(state->device); + spi_controller_release_bus(state->controller, state->device); + spi_controller_remove_device(state->controller, state->device); + releasePins(state); delete state; device_set_driver_data(device, nullptr); return ERROR_NONE; @@ -448,8 +459,8 @@ error_t reset(Device* device) { } const auto* config = getConfig(device); - if (config->pin_reset >= 0) { - pulseReset(config); + if (state->reset != nullptr) { + pulseReset(state, config); return ERROR_NONE; } diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.h b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.h index 3dd7fa7..ede1131 100644 --- a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.h +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Ili9341Spi.h @@ -9,22 +9,19 @@ /** * ILI9341 SPI display driver. * - * Used by the `ili9341-panel-module` kernel module in `Apps/MystifySpiDemo`, which owns the SPI - * bus, creates the display device from persisted settings, and tears it down again. The module is - * loaded from the app package at boot, so the panel exists as a normal DisplayApi device even when - * no app is running. + * Used by the `ili9341-panel-module` kernel module in `Apps/MystifySpiDemo`, which creates an + * esp32 SPI controller device for the panel's pins and makes this display a child of it. The + * module is loaded from the app package at boot, so the panel exists as a normal DisplayApi device + * even when no app is running. * - * The driver uses ESP-IDF's spi_master polling transactions directly instead of esp_lcd. That - * makes draw_bitmap() fully synchronous with no completion callback: an app/module ELF is not + * The driver uses the kernel SPI controller API with blocking transfers instead of esp_lcd. That + * keeps draw_bitmap() fully synchronous with no completion callback: an app/module ELF is not * guaranteed to be executable while the firmware's SPI ISR runs with caches disabled, and a * callback into it could crash there. */ /** What the panel looks like after MADCTL has been applied (its logical resolution). */ struct Ili9341SpiConfig { - /** ESP-IDF SPI host that is already initialised with the panel's bus pins (e.g. SPI2_HOST). */ - int spi_host = 1; - uint16_t width = 320; uint16_t height = 240; diff --git a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.cpp b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.cpp index 5a1b5b1..b224153 100644 --- a/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.cpp +++ b/Apps/MystifySpiDemo/ili9341-panel-module/main/Source/Panel.cpp @@ -1,12 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 #include "Panel.h" +#include "Esp32SpiConfig.h" #include "Ili9341Spi.h" #include #include #include #include +#include +#include #include #include @@ -17,16 +20,34 @@ namespace { constexpr auto* TAG = "Ili9341Panel"; constexpr auto* DEVICE_NAME = "ili9341-panel"; +constexpr auto* SPI_CONTROLLER_NAME = "ili9341-spi"; +constexpr auto* SPI_CONTROLLER_COMPATIBLE = "espressif,esp32-spi"; struct PanelRuntime { + // The SPI controller is created from the panel settings, so the module doesn't depend on a + // board devicetree node for the M-BUS pins. + struct Device controller {}; + struct Esp32SpiConfig controller_config {}; + bool controller_added = false; + struct Device device {}; struct Ili9341SpiConfig driver_config {}; - bool bus_initialized = false; bool device_added = false; }; PanelRuntime runtime; +struct GpioPinSpec makePin(struct Device* gpio_controller, int32_t pin) { + if (gpio_controller == nullptr || pin < 0) { + return GPIO_PIN_SPEC_NONE; + } + return GpioPinSpec { + gpio_controller, + static_cast(pin), + GPIO_FLAG_NONE, + }; +} + } // namespace error_t startPanel(const struct Ili9341PanelConfig& config) { @@ -34,33 +55,40 @@ error_t startPanel(const struct Ili9341PanelConfig& config) { return ERROR_NONE; } - spi_bus_config_t bus = {}; - bus.mosi_io_num = config.pin_mosi; - bus.miso_io_num = config.pin_miso; - bus.sclk_io_num = config.pin_sck; - bus.quadwp_io_num = -1; - bus.quadhd_io_num = -1; - bus.data4_io_num = -1; - bus.data5_io_num = -1; - bus.data6_io_num = -1; - bus.data7_io_num = -1; - bus.data_io_default_level = false; - bus.max_transfer_sz = static_cast(config.max_transfer_bytes); - bus.dma_burst_size = 0; - bus.flags = 0; - bus.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO; - bus.intr_flags = 0; - - const auto host = static_cast(config.spi_host); - esp_err_t bus_result = spi_bus_initialize(host, &bus, SPI_DMA_CH_AUTO); - if (bus_result != ESP_OK) { - LOG_E(TAG, "spi_bus_initialize failed: %s", esp_err_to_name(bus_result)); + struct Device* gpio0 = nullptr; + if (device_get_by_name("gpio0", &gpio0) != ERROR_NONE || gpio0 == nullptr) { + LOG_E(TAG, "The native GPIO controller (gpio0) was not found"); + return ERROR_NOT_FOUND; + } + + runtime.controller_config = Esp32SpiConfig { + .host = static_cast(config.spi_host), + .pin_sclk = makePin(gpio0, config.pin_sck), + .pin_mosi = makePin(gpio0, config.pin_mosi), + .pin_miso = makePin(gpio0, config.pin_miso), + .pin_wp = GPIO_PIN_SPEC_NONE, + .pin_hd = GPIO_PIN_SPEC_NONE, + .max_transfer_size = static_cast(config.max_transfer_bytes), + // CS is driven by the panel driver, so the controller owns no chip select pins. + .cs_gpios = nullptr, + .cs_gpios_count = 0, + .miso_pull_up = false, + }; + + runtime.controller = {}; + runtime.controller.name = SPI_CONTROLLER_NAME; + runtime.controller.address = 0; + runtime.controller.config = &runtime.controller_config; + runtime.controller.parent = nullptr; + runtime.controller.flags = 0; + + if (device_construct_add_start(&runtime.controller, SPI_CONTROLLER_COMPATIBLE) != ERROR_NONE) { + LOG_E(TAG, "Failed to start the SPI controller"); return ERROR_RESOURCE; } - runtime.bus_initialized = true; + runtime.controller_added = true; runtime.driver_config = Ili9341SpiConfig { - .spi_host = config.spi_host, .width = config.width, .height = config.height, .swap_xy = config.swap_xy, @@ -82,13 +110,15 @@ error_t startPanel(const struct Ili9341PanelConfig& config) { runtime.device.name = DEVICE_NAME; runtime.device.address = 0; runtime.device.config = &runtime.driver_config; - runtime.device.parent = nullptr; + runtime.device.parent = &runtime.controller; runtime.device.flags = 0; if (device_construct_add_start(&runtime.device, ILI9341_SPI_COMPATIBLE) != ERROR_NONE) { LOG_E(TAG, "Failed to start the ILI9341 panel"); - spi_bus_free(host); - runtime.bus_initialized = false; + device_stop(&runtime.controller); + device_remove(&runtime.controller); + device_destruct(&runtime.controller); + runtime.controller_added = false; return ERROR_RESOURCE; } runtime.device_added = true; @@ -118,13 +148,16 @@ error_t stopPanel() { } } - if (result == ERROR_NONE && runtime.bus_initialized) { - const auto host = static_cast(runtime.driver_config.spi_host); - if (spi_bus_free(host) != ESP_OK) { - LOG_W(TAG, "Could not free the SPI bus"); + // The controller can only stop once its peripherals (the panel) are gone. + if (result == ERROR_NONE && runtime.controller_added) { + error_t stop_result = device_stop(&runtime.controller); + error_t remove_result = stop_result == ERROR_NONE ? device_remove(&runtime.controller) : ERROR_INVALID_STATE; + error_t destruct_result = remove_result == ERROR_NONE ? device_destruct(&runtime.controller) : ERROR_INVALID_STATE; + if (destruct_result != ERROR_NONE) { + LOG_W(TAG, "Could not tear down the SPI controller"); result = ERROR_RESOURCE; } else { - runtime.bus_initialized = false; + runtime.controller_added = false; } } diff --git a/Apps/MystifySpiDemo/manifest.properties b/Apps/MystifySpiDemo/manifest.properties index a20905d..26d143b 100644 --- a/Apps/MystifySpiDemo/manifest.properties +++ b/Apps/MystifySpiDemo/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32p4 id=tactility.mystifyspidemo -version.name=0.5.0 -version.code=5 +version.name=0.6.0 +version.code=6 name=Mystify SPI Demo description=Adds a 2.8" ILI9341 SPI panel as a second display via an app-packaged driver module: Mystify demo, live settings, dashboard and terminal CLI. app.0.id=tactility.mystifyspidemo @@ -21,3 +21,4 @@ app.3.binary=ili9341panel app.3.headless=true app.3.hidden=false module.0.binary=ili9341-panel-module +requires.device.id=m5stack-tab5 diff --git a/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Application.cpp b/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Application.cpp index 2fbfb99..ee7b35d 100644 --- a/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Application.cpp +++ b/Apps/MystifySpiDemo/mystifyspidemo/main/Source/Application.cpp @@ -7,6 +7,10 @@ #include #include +#include +#include +#include + #include #include #include @@ -18,6 +22,7 @@ #include #include +#include #include #include #include @@ -37,10 +42,19 @@ constexpr int PRIMARY_MIRROR_HEIGHT = 360; constexpr int TARGET_FPS = 30; constexpr int64_t FRAME_BUDGET_US = 1'000'000 / TARGET_FPS; +// The primary mirror is a second full render plus a PPA scale and a 1280x720 present. It runs on +// its own task, so it doesn't sit in the panel's frame budget, but it still competes for memory +// bandwidth and is a mirror, so it doesn't need the panel's rate either. +constexpr int PRIMARY_FPS = 15; +// Plenty for drawScene()'s float maths and snprintf, plus the display driver's call depth. +constexpr uint32_t PRIMARY_TASK_STACK = 6144; constexpr int WALKER_COUNT = 2; constexpr int VERTEX_COUNT = 4; constexpr int TRAIL_LENGTH = 6; +// One extra snapshot so the previous frame's oldest echo (now at age TRAIL_LENGTH) can be erased +// without clearing the whole panel every frame. +constexpr int HISTORY_LENGTH = TRAIL_LENGTH + 1; // Normalised: both the 320x240 panel and the primary frame use the same simulation. constexpr float PLAY_MARGIN = 0.02f; @@ -64,8 +78,8 @@ struct Walker { float velocityY[VERTEX_COUNT]; uint16_t color = 0xFFFF; // history[0] is the current frame, history[TRAIL_LENGTH - 1] the oldest echo. - float historyX[TRAIL_LENGTH][VERTEX_COUNT]; - float historyY[TRAIL_LENGTH][VERTEX_COUNT]; + float historyX[HISTORY_LENGTH][VERTEX_COUNT]; + float historyY[HISTORY_LENGTH][VERTEX_COUNT]; }; void resetWalker(Walker& walker, uint16_t color) { @@ -76,7 +90,7 @@ void resetWalker(Walker& walker, uint16_t color) { walker.velocityX[vertex] = randomFloat(0.08f, 0.22f) * ((esp_random() & 1) != 0 ? 1.0f : -1.0f); walker.velocityY[vertex] = randomFloat(0.08f, 0.22f) * ((esp_random() & 1) != 0 ? 1.0f : -1.0f); } - for (int age = 0; age < TRAIL_LENGTH; age++) { + for (int age = 0; age < HISTORY_LENGTH; age++) { std::memcpy(walker.historyX[age], walker.x, sizeof(walker.x)); std::memcpy(walker.historyY[age], walker.y, sizeof(walker.y)); } @@ -104,7 +118,7 @@ void moveWalker(Walker& walker, float deltaSeconds) { } } - for (int age = TRAIL_LENGTH - 1; age > 0; age--) { + for (int age = HISTORY_LENGTH - 1; age > 0; age--) { std::memcpy(walker.historyX[age], walker.historyX[age - 1], sizeof(walker.x)); std::memcpy(walker.historyY[age], walker.historyY[age - 1], sizeof(walker.y)); } @@ -112,13 +126,47 @@ void moveWalker(Walker& walker, float deltaSeconds) { std::memcpy(walker.historyY[0], walker.y, sizeof(walker.y)); } -/** The whole surface is the play area: each display fills its own aspect ratio. */ -void drawScene(gfx::Surface& surface, const Walker* walkers, int fps) { - surface.clear(0x0000); - +/** + * The whole surface is the play area: each display fills its own aspect ratio. + * + * @param clear true for the first frame (and for targets that aren't rendered every frame); when + * false, only the previous frame's echoes and the two text bands are erased instead of clearing the + * whole frame, which keeps the panel's dirty area (and therefore SPI time) down. + */ +void drawScene(gfx::Surface& surface, const Walker* walkers, int fps, bool clear) { const float width = static_cast(surface.width()); const float height = static_cast(surface.height()); + const uint8_t textScale = static_cast( + std::min(4, std::max(1, std::min(surface.width(), surface.height()) / 150)) + ); + const int margin = 4 * textScale; + const int textHeight = static_cast(gfx::FONT_5X7.lineHeight) * textScale; + const int hintY = surface.height() - 11 * textScale; + + if (clear) { + surface.clear(0x0000); + } else { + // After the history shift, the previous frame's echoes are at ages 1..TRAIL_LENGTH. + for (int index = 0; index < WALKER_COUNT; index++) { + const Walker& walker = walkers[index]; + for (int age = TRAIL_LENGTH; age >= 1; age--) { + gfx::Point points[VERTEX_COUNT]; + for (int vertex = 0; vertex < VERTEX_COUNT; vertex++) { + points[vertex] = gfx::Point { + static_cast(walker.historyX[age][vertex] * width + 0.5f), + static_cast(walker.historyY[age][vertex] * height + 0.5f), + }; + } + surface.drawPolygon(points, VERTEX_COUNT, 0x0000); + } + } + // The static text is redrawn with an opaque background, but the fps line can get shorter + // and trails can cross both bands, so erase them before the new frame is drawn. + surface.fillRect(0, 0, surface.width(), margin + textHeight, 0x0000); + surface.fillRect(0, hintY, surface.width(), surface.height() - hintY, 0x0000); + } + for (int index = 0; index < WALKER_COUNT; index++) { const Walker& walker = walkers[index]; // Draw the oldest echo first so the newest polygon lands on top. @@ -137,17 +185,12 @@ void drawScene(gfx::Surface& surface, const Walker* walkers, int fps) { } } - const uint8_t textScale = static_cast( - std::min(4, std::max(1, std::min(surface.width(), surface.height()) / 150)) - ); - const int margin = 4 * textScale; - char info[48]; std::snprintf(info, sizeof(info), "%dx%d %d fps", surface.width(), surface.height(), fps); surface.drawText(margin, margin, info, gfx::FONT_5X7, 0x9CF3, 0x0000, textScale); surface.drawText( margin, - surface.height() - 11 * textScale, + hintY, "any input exits", gfx::FONT_5X7, 0x632C, @@ -156,6 +199,65 @@ void drawScene(gfx::Surface& surface, const Walker* walkers, int fps) { ); } +/** + * The primary mirror is rendered on its own task: a 640x360 draw plus PPA scale and 720x1280 + * present takes ~29 ms, which would otherwise push every third panel frame past the 30 fps + * budget. The task keeps a snapshot of the walkers and renders independently. + */ +struct PrimaryRenderer { + gfx::Target* target = nullptr; + Walker walkers[WALKER_COUNT]; + int fps = 0; + gfx::Stats stats {}; + // A FreeRTOS mutex rather than a portMUX spinlock: portENTER_CRITICAL expands to + // vPortEnterCriticalMultiCore on this dual-core target, which the firmware's ELF loader + // doesn't export (see Doom's audio mixer). The guarded copies are a few hundred bytes. + SemaphoreHandle_t mutex = nullptr; + SemaphoreHandle_t signal = nullptr; + SemaphoreHandle_t finished = nullptr; + std::atomic stop { false }; +}; + +bool lockMirror(PrimaryRenderer& renderer) { + return renderer.mutex != nullptr && + xSemaphoreTake(renderer.mutex, pdMS_TO_TICKS(100)) == pdTRUE; +} + +void primaryRenderTask(void* argument) { + auto* renderer = static_cast(argument); + Walker snapshot[WALKER_COUNT]; + int snapshotFps = 0; + + while (true) { + if (xSemaphoreTake(renderer->signal, portMAX_DELAY) != pdTRUE) { + break; + } + if (renderer->stop.load()) { + break; + } + + // Take the latest simulation state. If the panel task is holding the mutex for longer + // than expected, redrawing the previous snapshot once is better than stalling here. + if (lockMirror(*renderer)) { + std::memcpy(snapshot, renderer->walkers, sizeof(snapshot)); + snapshotFps = renderer->fps; + xSemaphoreGive(renderer->mutex); + } + + renderer->target->render([&](gfx::Surface& surface) { + drawScene(surface, snapshot, snapshotFps, true); + }); + + if (lockMirror(*renderer)) { + renderer->stats = renderer->target->stats(); + xSemaphoreGive(renderer->mutex); + } + } + + xSemaphoreGive(renderer->finished); + vTaskDelete(nullptr); +} + bool takeExit(struct AppEventSubscription& subscription, AnyInput& input) { bool shouldExit = false; struct AppEvent event {}; @@ -280,8 +382,10 @@ const char* runApplication(struct Device* display, AnyInput& input) { int fps = 0; int64_t measurement_start_us = esp_timer_get_time(); - auto render_scene = [&](gfx::Surface& surface) { - drawScene(surface, walkers, fps); + bool panel_first_frame = true; + auto render_panel = [&](gfx::Surface& surface) { + drawScene(surface, walkers, fps, panel_first_frame); + panel_first_frame = false; }; // Unmistakable write-path check before the animation: red, green, blue, white, black. Both @@ -300,14 +404,31 @@ const char* runApplication(struct Device* display, AnyInput& input) { tt::kernel::delayTicks(tt::kernel::millisToTicks(150)); } + // Hand the mirror to its own task so a 29 ms primary frame no longer sits inside the panel's + // 33 ms budget. It renders the same simulation from a snapshot; it never touches the panel + // target or the walkers themselves. + PrimaryRenderer mirror; + mirror.target = &primary; + mirror.mutex = xSemaphoreCreateMutex(); + mirror.signal = xSemaphoreCreateBinary(); + mirror.finished = xSemaphoreCreateBinary(); + bool mirror_task = primary.valid() && mirror.mutex != nullptr && mirror.signal != nullptr && + mirror.finished != nullptr && + xTaskCreatePinnedToCore(primaryRenderTask, "primary", PRIMARY_TASK_STACK, &mirror, 3, nullptr, 1) == pdPASS; + if (!mirror_task && primary.valid()) { + ESP_LOGW(TAG, "Primary mirror task unavailable; showing the SPI panel only"); + } + ESP_LOGI( TAG, "Running Mystify at up to %d fps on the panel%s", TARGET_FPS, - primary.valid() ? " and the primary display" : "" + mirror_task ? " and the primary display" : "" ); int64_t last_frame_us = esp_timer_get_time(); + int64_t last_primary_us = 0; + const int64_t primary_budget_us = 1'000'000 / PRIMARY_FPS; while (!takeExit(subscription, input)) { const int64_t frame_start_us = esp_timer_get_time(); @@ -319,24 +440,52 @@ const char* runApplication(struct Device* display, AnyInput& input) { moveWalker(walker, deltaSeconds); } - lcd_target.render(render_scene); - if (primary.valid()) { - primary.render(render_scene); + lcd_target.render(render_panel); + if (mirror_task && frame_start_us - last_primary_us >= primary_budget_us) { + if (lockMirror(mirror)) { + std::memcpy(mirror.walkers, walkers, sizeof(mirror.walkers)); + mirror.fps = fps; + xSemaphoreGive(mirror.mutex); + } + // If the task hasn't finished the previous frame the binary semaphore is already + // set and this give is a no-op, which drops a mirror frame instead of queueing it. + xSemaphoreGive(mirror.signal); + last_primary_us = frame_start_us; } frames_this_second++; const int64_t now_us = esp_timer_get_time(); if (now_us - measurement_start_us >= 1'000'000) { fps = frames_this_second; - const gfx::Stats& stats = lcd_target.stats(); - ESP_LOGI( - TAG, - "%d fps, %d dirty px, present %d us, total %d us", - fps, - stats.dirtyPixels, - stats.presentUs, - stats.totalUs - ); + const gfx::Stats& panel_stats = lcd_target.stats(); + if (mirror_task) { + gfx::Stats primary_stats {}; + if (lockMirror(mirror)) { + primary_stats = mirror.stats; + xSemaphoreGive(mirror.mutex); + } + ESP_LOGI( + TAG, + "%d fps, panel %d px/%d rects, present %d us/total %d us, primary present %d us/total %d us", + fps, + panel_stats.dirtyPixels, + panel_stats.dirtyRects, + panel_stats.presentUs, + panel_stats.totalUs, + primary_stats.presentUs, + primary_stats.totalUs + ); + } else { + ESP_LOGI( + TAG, + "%d fps, panel %d px/%d rects, present %d us/total %d us", + fps, + panel_stats.dirtyPixels, + panel_stats.dirtyRects, + panel_stats.presentUs, + panel_stats.totalUs + ); + } frames_this_second = 0; measurement_start_us = now_us; } @@ -347,12 +496,30 @@ const char* runApplication(struct Device* display, AnyInput& input) { } } + // Join the mirror task before touching either target again: it may be mid-render, and the + // final clear and the target's destructor both run on this task. + if (mirror_task) { + mirror.stop.store(true); + xSemaphoreGive(mirror.signal); + xSemaphoreTake(mirror.finished, portMAX_DELAY); + } + // Leave both panels black: their images otherwise stay on screen after the app exits. lcd_target.render([](gfx::Surface& surface) { surface.clear(0x0000); }); if (primary.valid()) { primary.render([](gfx::Surface& surface) { surface.clear(0x0000); }); } + if (mirror.finished != nullptr) { + vSemaphoreDelete(mirror.finished); + } + if (mirror.signal != nullptr) { + vSemaphoreDelete(mirror.signal); + } + if (mirror.mutex != nullptr) { + vSemaphoreDelete(mirror.mutex); + } + device_put(lcd); releasePanelApi(api); diff --git a/Libraries/Gfx/Source/Polygon.cpp b/Libraries/Gfx/Source/Polygon.cpp index 4decd9d..9bad063 100644 --- a/Libraries/Gfx/Source/Polygon.cpp +++ b/Libraries/Gfx/Source/Polygon.cpp @@ -36,11 +36,22 @@ void Surface::drawPolygon(const Point* points, int count, uint16_t rgb565) { if (box.empty()) { return; } - markDirtyRect(box.x0, box.y0, box.x1, box.y1); + // Mark each edge's bounds rather than the whole polygon's: these are outlines, so a quad's + // full bounding box would make a dirty-region target present most of the screen for a few thin + // lines. for (int i = 0; i < count; i++) { const Point& a = points[i]; const Point& b = points[(i + 1) % count]; + const Rect edge = Rect { + std::min(a.x, b.x), + std::min(a.y, b.y), + std::max(a.x, b.x) + 1, + std::max(a.y, b.y) + 1, + }.intersect(clip_); + if (!edge.empty()) { + markDirtyRect(edge.x0, edge.y0, edge.x1, edge.y1); + } lineRaw(a.x, a.y, b.x, b.y, rgb565); } }