<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Chris's hyperfixations]]></title><description><![CDATA[Chris's hyperfixations]]></description><link>https://cdebray.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 12:00:18 GMT</lastBuildDate><atom:link href="https://cdebray.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Modular Starter Kit for M5StickC-Plus2: From Messy Code to Clean Architecture]]></title><description><![CDATA[Why Another M5Stack Project?
When I first got my M5StickC-Plus2, I was excited to build something cool. But like many developers, I quickly hit a wall of... boring setup work.
You know the drill: configuring buttons, managing display coordinates, han...]]></description><link>https://cdebray.hashnode.dev/building-a-modular-starter-kit-for-m5stickc-plus2-from-messy-code-to-clean-architecture</link><guid isPermaLink="true">https://cdebray.hashnode.dev/building-a-modular-starter-kit-for-m5stickc-plus2-from-messy-code-to-clean-architecture</guid><category><![CDATA[m5stack]]></category><category><![CDATA[iot]]></category><category><![CDATA[Developer]]></category><category><![CDATA[development]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[arduino]]></category><category><![CDATA[cpp]]></category><category><![CDATA[C++]]></category><category><![CDATA[hardware]]></category><dc:creator><![CDATA[Christopher Debray]]></dc:creator><pubDate>Tue, 28 Oct 2025 19:52:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761690051180/61220c6d-bc47-4d94-8fd7-2a099681ad2e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-why-another-m5stack-project">Why Another M5Stack Project?</h2>
<p>When I first got my M5StickC-Plus2, I was excited to build something cool. But like many developers, I quickly hit a wall of... boring setup work.</p>
<p>You know the drill: configuring buttons, managing display coordinates, handling menus, dealing with power management, setting up timers. Before I could even start on the <em>fun</em> part of my project, I had to write hundreds of lines of infrastructure code.</p>
<p>Libraries help, but they come with their own problems:</p>
<ul>
<li><p><strong>Black boxes</strong>: You can't see or modify how they work internally</p>
</li>
<li><p><strong>Over-abstraction</strong>: Sometimes you need fine-grained control</p>
</li>
<li><p><strong>Learning curve</strong>: Each library has its own API to learn</p>
</li>
<li><p><strong>Dependencies</strong>: One library pulls in five others</p>
</li>
</ul>
<p>I wanted something different: <strong>a starter kit where you own all the code</strong>.</p>
<h2 id="heading-the-philosophy-a-foundation-not-a-framework">The Philosophy: A Foundation, Not a Framework</h2>
<p>This project isn't a library you import. It's a <strong>starting point you customize</strong>.</p>
<p>Think of it like this:</p>
<ul>
<li><p><strong>Library</strong>: "Here's a menu system, use these methods"</p>
</li>
<li><p><strong>This starter</strong>: "Here's how I built a menu system, change whatever you want"</p>
</li>
</ul>
<p>You get:</p>
<ul>
<li><p>✅ Full source code you can read and understand</p>
</li>
<li><p>✅ Working examples you can modify</p>
</li>
<li><p>✅ Architectural patterns you can extend</p>
</li>
<li><p>✅ No hidden dependencies or magic</p>
</li>
</ul>
<p>If you don't like how the menu scrolling works? Change it. Want different colors? Modify the display handler. Need a different button layout? Update the controls.</p>
<h2 id="heading-the-journey-from-arduino-ide-to-platformio">The Journey: From Arduino IDE to PlatformIO</h2>
<p>I started this project in Arduino IDE (as many do), but quickly switched to <strong>VSCode + PlatformIO</strong>. Here's why:</p>
<h3 id="heading-arduino-ide-pain-points">Arduino IDE Pain Points</h3>
<pre><code class="lang-cpp"><span class="hljs-comment">// Where is this function defined?</span>
<span class="hljs-comment">// Which library does this come from?</span>
<span class="hljs-comment">// Good luck finding it...</span>
M5.Lcd.setCursor(<span class="hljs-number">10</span>, <span class="hljs-number">80</span>);
</code></pre>
<h3 id="heading-platformio-wins">PlatformIO Wins</h3>
<ul>
<li><p><strong>IntelliSense</strong>: Auto-completion that actually works</p>
</li>
<li><p><strong>Go to Definition</strong>: Jump to any function's source</p>
</li>
<li><p><strong>Project Structure</strong>: Proper file organization (the biggest issue for me)</p>
</li>
<li><p><strong>Library Management</strong>: Clear dependency handling</p>
</li>
<li><p><strong>Modern C++</strong>: Full C++11/14/17 support</p>
</li>
</ul>
<p>The switch took an hour. It saved me dozens of hours afterward.</p>
<h2 id="heading-key-architecture-decisions">Key Architecture Decisions</h2>
<h3 id="heading-1-the-page-system-lifecycle-management">1. The Page System: Lifecycle Management</h3>
<p>Early on, I realized I needed multiple "screens" or "pages". But switching between them was messy:</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// ❌ The messy way</span>
<span class="hljs-keyword">if</span> (currentPage == <span class="hljs-number">0</span>) {
    drawClock();
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (currentPage == <span class="hljs-number">1</span>) {
    drawMenu();
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (currentPage == <span class="hljs-number">2</span>) {
    drawSettings();
}
</code></pre>
<p>I needed a proper lifecycle. Enter the <strong>PageManager</strong>:</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PageBase</span> {</span>
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;    <span class="hljs-comment">// Called when entering page</span>
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">loop</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;     <span class="hljs-comment">// Called every frame</span>
    <span class="hljs-function"><span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">cleanup</span><span class="hljs-params">()</span> </span>= <span class="hljs-number">0</span>;  <span class="hljs-comment">// Called when leaving page</span>
};
</code></pre>
<p>Now each page manages itself:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">ClockPage::setup</span><span class="hljs-params">()</span> </span>{
    display-&gt;clearScreen();
    clockHandler-&gt;drawClock(<span class="hljs-number">0</span>);
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">ClockPage::loop</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">if</span> (hasActiveMenu()) <span class="hljs-keyword">return</span>;  <span class="hljs-comment">// Pause if menu is open</span>
    <span class="hljs-comment">// Update clock every second</span>
}
</code></pre>
<p><strong>Lesson learned</strong>: Give each component its own lifecycle. Don't manage everything from <code>main()</code>.</p>
<h3 id="heading-2-the-menu-stack-nested-menus-done-right">2. The Menu Stack: Nested Menus Done Right</h3>
<p>Menus were surprisingly hard. I wanted:</p>
<ul>
<li><p>A main menu</p>
</li>
<li><p>Submenus (Settings → Display Settings → Brightness)</p>
</li>
<li><p>A "back" button that works correctly</p>
</li>
</ul>
<p>The solution? A <strong>stack</strong> (Last In, First Out):</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MenuManager</span> {</span>
<span class="hljs-keyword">private</span>:
    MenuHandler* menuStack[MAX_MENU_STACK];
    <span class="hljs-keyword">int</span> stackSize;

<span class="hljs-keyword">public</span>:
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">pushMenu</span><span class="hljs-params">(MenuHandler* menu)</span> </span>{
        menuStack[stackSize++] = menu;
        menu-&gt;draw();
    }

    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">popMenu</span><span class="hljs-params">()</span> </span>{
        stackSize--;
        <span class="hljs-keyword">if</span> (stackSize &gt; <span class="hljs-number">0</span>) {
            menuStack[stackSize - <span class="hljs-number">1</span>]-&gt;draw();  <span class="hljs-comment">// Redraw previous menu</span>
        }
    }
};
</code></pre>
<p>Now submenus just work:</p>
<pre><code class="lang-plaintext">Clock Page
  → Open Menu
    → Settings
      → Display
        → [Back]
      → [Back]
    → [Back]
  Clock Page (restored)
</code></pre>
<p><strong>Lesson learned</strong>: Choose the right data structure. A stack naturally handles nested navigation.</p>
<h3 id="heading-3-the-pointer-function-vs-stdfunction-saga">3. The Pointer Function vs std::function Saga</h3>
<p>This was a <strong>4-hour debugging session</strong> that taught me a crucial C++ lesson.</p>
<p>I wanted menu callbacks that could access class members:</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// I wanted to do this:</span>
mainMenu-&gt;addItem(<span class="hljs-string">"Start Timer"</span>, [<span class="hljs-keyword">this</span>]() {
    clockHandler-&gt;startTimer();  <span class="hljs-comment">// Access class member</span>
});
</code></pre>
<p>But I got errors:</p>
<pre><code class="lang-plaintext">error: no suitable conversion from lambda to void (*)()
</code></pre>
<p>The problem? My MenuItem struct used old C-style function pointers:</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// ❌ Old way (C-style)</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">MenuItem</span> {</span>
    <span class="hljs-keyword">void</span> (*callback)();  <span class="hljs-comment">// Can't capture 'this'!</span>
};
</code></pre>
<p>The fix? Modern C++ <code>std::function</code>:</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// ✅ New way (C++11)</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">MenuItem</span> {</span>
    <span class="hljs-built_in">std</span>::function&lt;<span class="hljs-keyword">void</span>()&gt; callback;  <span class="hljs-comment">// Can capture anything!</span>
};
</code></pre>
<p>Now this works:</p>
<pre><code class="lang-cpp">mainMenu-&gt;addItem(<span class="hljs-string">"Settings"</span>, [<span class="hljs-keyword">this</span>]() {
    openSettingsSubmenu();  <span class="hljs-comment">// 'this' captured, works perfectly</span>
});
</code></pre>
<p><strong>Lesson learned</strong>: Use <code>std::function</code> for callbacks in modern C++. It's more flexible and handles lambdas with captures.</p>
<h3 id="heading-4-display-positioning-no-more-magic-numbers">4. Display Positioning: No More Magic Numbers</h3>
<p>Early code looked like this:</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// ❌ What does this even mean?</span>
M5.Lcd.setCursor(<span class="hljs-number">10</span>, <span class="hljs-number">80</span>);
M5.Lcd.setTextSize(<span class="hljs-number">3</span>);
M5.Lcd.print(<span class="hljs-string">"Hello"</span>);
</code></pre>
<p>I created the <strong>DisplayHandler</strong> to abstract positions:</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// ✅ Semantic and clear</span>
display-&gt;displayMainTitle(<span class="hljs-string">"Hello"</span>);
display-&gt;displaySubtitle(<span class="hljs-string">"Subtitle"</span>);
display-&gt;displayStatus(<span class="hljs-string">"Ready"</span>, MSG_SUCCESS);
</code></pre>
<p>Behind the scenes:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">displayMainTitle</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span>* text, MessageType type)</span> </span>{
    M5.Lcd.setTextSize(SIZE_TITLE);  <span class="hljs-comment">// Consistent size</span>
    M5.Lcd.setTextColor(getColorForType(type));

    <span class="hljs-keyword">int</span> x = (SCREEN_WIDTH - textWidth) / <span class="hljs-number">2</span>;  <span class="hljs-comment">// Auto-center</span>
    <span class="hljs-keyword">int</span> y = ZONE_CENTER_Y - <span class="hljs-number">20</span>;

    M5.Lcd.setCursor(x, y);
    M5.Lcd.print(text);
}
</code></pre>
<p><strong>Lesson learned</strong>: Abstract low-level details. Your future self will thank you.</p>
<h3 id="heading-5-deep-sleep-the-3-hour-power-management-bug">5. Deep Sleep: The 3-Hour Power Management Bug</h3>
<p>The M5StickC-Plus2 has great battery life... if you use deep sleep correctly.</p>
<p>My first attempt:</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// ❌ This crashes the device on wake-up</span>
esp_deep_sleep_start();
</code></pre>
<p>After diving into documentation and forums, I found the issue: <strong>GPIO4 must stay HIGH during sleep</strong> or the device loses power.</p>
<p>The working solution:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">M5deepSleep</span><span class="hljs-params">(<span class="hljs-keyword">uint64_t</span> microseconds)</span> </span>{
    <span class="hljs-comment">// CRITICAL: Keep power pin high</span>
    pinMode(<span class="hljs-number">4</span>, OUTPUT);
    digitalWrite(<span class="hljs-number">4</span>, HIGH);
    gpio_hold_en(GPIO_NUM_4);
    gpio_deep_sleep_hold_en();

    esp_sleep_enable_timer_wakeup(microseconds);
    esp_deep_sleep_start();
}
</code></pre>
<p>This powers my Pomodoro timer: 25 minutes of sleep, wake up, beep alarm, show clock.</p>
<p><strong>Lesson learned</strong>: Hardware-specific quirks require hardware-specific solutions. Don't always assume standard APIs work out of the box.</p>
<h2 id="heading-button-controls-finding-the-ergonomic-sweet-spot">Button Controls: Finding the Ergonomic Sweet Spot</h2>
<p>The M5StickC-Plus2 has three buttons:</p>
<pre><code class="lang-plaintext">      ______PWR          (side)
                    A    (front)
      ___B_____          (side, opposite)
</code></pre>
<p>After testing different layouts, I settled on:</p>
<p><strong>No menu active</strong>:</p>
<ul>
<li><p>PWR: Change page</p>
</li>
<li><p>A: Open menu</p>
</li>
<li><p>B: Page-specific action (e.g., start timer on double click)</p>
</li>
</ul>
<p><strong>Menu active</strong>:</p>
<ul>
<li><p>PWR: Navigate down</p>
</li>
<li><p>A: Select item</p>
</li>
<li><p>B (short): Navigate up</p>
</li>
<li><p>B (long hold): Close menu</p>
</li>
</ul>
<p>Why this layout?</p>
<ul>
<li><p><strong>Side buttons for navigation</strong>: Easier to press while holding device</p>
</li>
<li><p><strong>Center button for actions</strong>: Most important button in prime position</p>
</li>
<li><p><strong>Long press for "back"</strong>: Prevents accidental exits</p>
</li>
</ul>
<p><strong>Lesson learned</strong>: Button ergonomics matter. Test on actual hardware, not just in your head.</p>
<h2 id="heading-the-tech-stack">The Tech Stack</h2>
<ul>
<li><p><strong>Platform</strong>: M5StickC-Plus2</p>
</li>
<li><p><strong>IDE</strong>: VSCode + PlatformIO</p>
</li>
<li><p><strong>Language</strong>: C++ (C++11 features)</p>
</li>
<li><p><strong>Libraries</strong>: M5Unified</p>
</li>
<li><p><strong>Architecture</strong>: OOP with composition pattern</p>
</li>
</ul>
<p>Key files:</p>
<pre><code class="lang-plaintext">lib/
├── display_handler.h      # Display abstraction
├── menu_handler.h         # Individual menu logic
├── menu_manager.h         # Menu stack
├── page_manager.h         # Page lifecycle
├── clock_handler.h        # Time &amp; timers
├── battery_handler.h      # Power management
└── pages/
    ├── page_base.h        # Abstract base class
    └── clock_page.h       # Default clock page
</code></pre>
<h2 id="heading-what-id-do-differently">What I'd Do Differently</h2>
<h3 id="heading-1-start-with-stdfunction">1. Start with std::function</h3>
<p>Don't use C-style function pointers for callbacks. Go straight to <code>std::function&lt;void()&gt;</code>.<br />Althought it is more consuming than the pointers, but i didn’t have time to figure a better option (for now)</p>
<h3 id="heading-2-test-deep-sleep-early">2. Test Deep Sleep Early</h3>
<p>Don't wait until the end to test power management. It's hardware-dependent and can break everything.</p>
<h3 id="heading-3-design-button-layout-on-paper">3. Design Button Layout on Paper</h3>
<p>Sketch the button layout before writing code. Changing it later affects everything.</p>
<h3 id="heading-4-use-platformio-from-day-1">4. Use PlatformIO from Day 1</h3>
<p>Don't start in Arduino IDE. The migration takes time and breaks things.</p>
<h2 id="heading-what-worked-really-well">What Worked Really Well</h2>
<h3 id="heading-1-composition-over-inheritance">1. Composition Over Inheritance</h3>
<p>Every page gets a <code>DisplayHandler*</code> and <code>MenuManager*</code>. They don't inherit display logic—they compose it.</p>
<h3 id="heading-2-clear-separation-of-concerns">2. Clear Separation of Concerns</h3>
<ul>
<li><p><code>*_handler.h</code>: Focused, reusable components</p>
</li>
<li><p><code>*_manager.h</code>: Complex orchestration</p>
</li>
<li><p><code>*_utils.h</code>: Utility functions</p>
</li>
</ul>
<h3 id="heading-3-lambda-callbacks">3. Lambda Callbacks</h3>
<p>Being able to write <code>[this]() { myMethod(); }</code> inline makes code so much cleaner than separate callback functions.</p>
<h3 id="heading-4-the-base-page-pattern">4. The Base Page Pattern</h3>
<p>Every page inherits from <code>PageBase</code>, which provides menu management for free. No duplicate code.</p>
<h2 id="heading-try-it-yourself">Try It Yourself</h2>
<p>The complete starter kit is <a target="_blank" href="https://github.com/ChristopherDebray/M5StickC-Plus2-starter">available on GitHub</a>. Clone it, upload to your M5StickC-Plus2, and you'll have:</p>
<ul>
<li><p>✅ A working clock page</p>
</li>
<li><p>✅ Battery indicator</p>
</li>
<li><p>✅ Menu system with submenus</p>
</li>
<li><p>✅ Page navigation</p>
</li>
<li><p>✅ Pomodoro timer</p>
</li>
<li><p>✅ All the code to modify</p>
</li>
</ul>
<p>Want to build a fitness tracker? Keep the page system, replace the clock logic.</p>
<p>Building a game? Use the menu system for your settings, swap in your game loop.</p>
<p>Creating an IoT dashboard? The display handler abstracts all the positioning for you.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Building this starter kit taught me that <strong>good architecture is invisible</strong>. When it works, you don't think about pages or menus—you just build features.</p>
<p>That's the goal: give you the boring stuff so you can focus on the interesting stuff.</p>
<p>The M5StickC-Plus2 is a fantastic device. With the right foundation, you can build something amazing in a weekend instead of spending that weekend setting up infrastructure.</p>
<p>Now go build something cool. 🚀</p>
<hr />
<h2 id="heading-resources">Resources</h2>
<ul>
<li><a target="_blank" href="https://github.com/m5stack/M5Unified">M5Unified</a></li>
</ul>
<hr />
<p><em>What would you build with this starter kit? Let me know in the comments!</em></p>
<hr />
<p><em>PS: Sorry but no images for now, i don’t have a camera that can capture the small screen of the M5 without rendering it’s content poorly</em></p>
]]></content:encoded></item></channel></rss>