<?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" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Gyokhan Kochmarla]]></title><description><![CDATA[a embedded software engineer's handbook (runs on a RPi 5)]]></description><link>https://gyokhan.com/</link><image><url>https://gyokhan.com/favicon.png</url><title>Gyokhan Kochmarla</title><link>https://gyokhan.com/</link></image><generator>Ghost 5.75</generator><lastBuildDate>Tue, 23 Jun 2026 16:34:33 GMT</lastBuildDate><atom:link href="https://gyokhan.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Case Study on the Design Patterns for Embedded Systems using Flipper One's Firmware]]></title><description><![CDATA[<p>This document was created by OpenCode Zen Big Pickle LLM with my guidance.</p><hr><h2 id="1-architectural-patterns">1. Architectural Patterns</h2>
<h3 id="11-microkernel-service-oriented-architecture">1.1 Microkernel / Service-Oriented Architecture</h3>
<p><strong>Concept</strong>: A minimal core (the kernel) that loads and coordinates independent modules (services). Each service is an isolated FreeRTOS task responsible for one domain.</p>
<p><strong>Implementation</strong> (<code>applications/applications.h</code>):</p>
<pre><code class="language-c">// All</code></pre>]]></description><link>https://gyokhan.com/lectures-on-the-design-patterns-for-embedded-systems-using-flipper-ones-firmware/</link><guid isPermaLink="false">6a157b1ab8b3ea0001a580c0</guid><category><![CDATA[ai-written]]></category><category><![CDATA[cheatsheets]]></category><category><![CDATA[embedded systems]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Tue, 26 May 2026 10:53:07 GMT</pubDate><content:encoded><![CDATA[<p>This document was created by OpenCode Zen Big Pickle LLM with my guidance.</p><hr><h2 id="1-architectural-patterns">1. Architectural Patterns</h2>
<h3 id="11-microkernel-service-oriented-architecture">1.1 Microkernel / Service-Oriented Architecture</h3>
<p><strong>Concept</strong>: A minimal core (the kernel) that loads and coordinates independent modules (services). Each service is an isolated FreeRTOS task responsible for one domain.</p>
<p><strong>Implementation</strong> (<code>applications/applications.h</code>):</p>
<pre><code class="language-c">// All services, apps, and CLI commands are declared as static tables
// using a registry macro system:

typedef struct {
    const char* name;
    const char* appid;
    uint32_t stack_size;
    FuriThreadCallback app;
    FlipperInternalApplicationFlag flags;
} FlipperInternalApplication;

// Services are long-lived daemon threads:
#define FLIPPER_SERVICES \
    DEF(GUI, &quot;GuiSrv&quot;, 4096, gui_srv, FlipperInternalApplicationFlagInsomniaSafe) \
    DEF(INPUT, &quot;InputSrv&quot;, 2048, input_srv, FlipperInternalApplicationFlagInsomniaSafe) \
    DEF(POWER, &quot;PowerSrv&quot;, 2048, power_srv, FlipperInternalApplicationFlagInsomniaSafe) \
    DEF(HAPTIC, &quot;HapticSrv&quot;, 2048, haptic_srv, FlipperInternalApplicationFlagInsomniaSafe) \
    /* ... 18 services total */

// Main apps are user-launched:
#define FLIPPER_APPS \
    DEF(CPU, &quot;CPU App&quot;, 2048, cpu_app, FlipperInternalApplicationFlagDefault)

// Autorun apps run at boot:
#define FLIPPER_AUTORUN_APPS \
    DEF(SELF_CHECK, &quot;Self Check&quot;, 2048, self_check, FlipperInternalApplicationFlagDefault)
</code></pre>
<p><strong>Why it matters</strong>: Every service is self-contained with its own thread, stack, and event loop. Adding a new feature means adding one entry to the table and writing the service module &#x2014; no modification to existing code. This is the <strong>Open/Closed Principle</strong> in embedded C.</p>
<p><strong>Lesson for your designs</strong>: Use a central registry of all tasks/modules. Make it trivial to add/remove features by editing one table.</p>
<hr>
<h3 id="12-reactor-event-loop">1.2 Reactor / Event Loop</h3>
<p><strong>Concept</strong>: A single-threaded, non-blocking event dispatcher. The thread blocks until events arrive, then dispatches them to registered handlers. Inspired by epoll/kqueue.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/event_loop.h</code>):</p>
<pre><code class="language-c">// Each service creates its own event loop:
FuriEventLoop* loop = furi_event_loop_alloc();

// Subscribe to various event sources:
furi_event_loop_subscribe_message_queue(loop, msg_queue, 
    FuriEventLoopEventIn, my_callback, ctx);
furi_event_loop_subscribe_timer(loop, timer, my_timer_cb, ctx);
furi_event_loop_subscribe_event_flag(loop, flag, 
    FuriEventLoopEventFlagEdge, my_flag_cb, ctx);

// Run forever:
furi_event_loop_run(loop);  // Blocks, dispatches events
</code></pre>
<p><strong>Key design insight</strong>: The event loop uses a <strong>Strategy pattern</strong> internally via <code>FuriEventLoopContract</code>:</p>
<pre><code class="language-c">// Each object type provides its own contract (vtable):
typedef struct {
    const FuriEventLoopContractGetLink get_link;
    const FuriEventLoopContractGetLevel get_level;
} FuriEventLoopContract;

// Message queues, event flags, stream buffers, semaphores each 
// implement these differently
extern const FuriEventLoopContract furi_event_loop_message_queue_contract;
extern const FuriEventLoopContract furi_event_loop_event_flag_contract;
</code></pre>
<p>It supports <strong>level-triggered</strong> (fire while condition holds) and <strong>edge-triggered</strong> (fire once per state change) &#x2014; directly modeled after Linux epoll.</p>
<p><strong>Why it matters</strong>: Instead of each service having a custom polling loop, they all use the same event loop pattern. This gives:</p>
<ul>
<li>Uniform structure across all modules</li>
<li>No busy-waiting (power efficient)</li>
<li>Clean separation of event sources from event handlers</li>
<li>Thread-safe deferral via <code>furi_event_loop_pend_callback()</code></li>
</ul>
<p><strong>Lesson for your designs</strong>: Build or adopt a lightweight reactor pattern. It eliminates the common firmware anti-pattern of ad-hoc polling loops and makes every module structurally consistent.</p>
<hr>
<h3 id="13-layered-architecture">1.3 Layered Architecture</h3>
<p><strong>Concept</strong>: Strict separation into layers, each depending only on the layer below.</p>
<p><strong>Implementation</strong> (entire project):</p>
<pre><code>&#x250C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2510;
&#x2502;            Applications Layer           &#x2502;
&#x2502;  (gui, input, power, cli, desktop, ...) &#x2502;
&#x251C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2524;
&#x2502;            Service Layer                &#x2502;
&#x2502;  (thread lifecycle, IPC, records, ...)  &#x2502;
&#x251C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2524;
&#x2502;         FURI Framework Layer            &#x2502;
&#x2502;  (event_loop, pubsub, thread, string)   &#x2502;
&#x251C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2524;
&#x2502;        HAL Abstraction Layer            &#x2502;
&#x2502;  (furi_hal_i2c, gpio, spi, serial)      &#x2502;
&#x251C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2524;
&#x2502;  &#x250C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2510;   &#x2502;
&#x2502;  &#x2502;  Driver Layer                    &#x2502;   &#x2502;
&#x2502;  &#x2502;  (bq25792, drv2605l, iqs7211e)  &#x2502;   &#x2502;
&#x2502;  &#x2514;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2518;   &#x2502;
&#x251C;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2524;
&#x2502;   Raspberry Pi Pico SDK + Hardware      &#x2502;
&#x2514;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2500;&#x2518;
</code></pre>
<p>Each driver (e.g., <code>BQ25792</code> charger) only calls <code>furi_hal_i2c</code> &#x2014; it never touches the I2C registers directly. Each service (e.g., <code>PowerSrv</code>) only calls the driver API &#x2014; it never touches <code>furi_hal_i2c</code>.</p>
<p><strong>Why it matters</strong>: You can swap hardware (e.g., a different charger IC) by replacing only the driver layer. The service and HAL layers remain unchanged.</p>
<p><strong>Lesson</strong>: Draw your layer diagram before writing code. Enforce strict dependency direction.</p>
<hr>
<h2 id="2-creational-patterns">2. Creational Patterns</h2>
<h3 id="21-factory-method-via-macro-registry">2.1 Factory Method (via Macro Registry)</h3>
<p><strong>Concept</strong>: A centralized function that creates objects (threads) from static descriptors.</p>
<p><strong>Implementation</strong> (<code>lib/furi/flipper.c</code>):</p>
<pre><code class="language-c">void flipper_init() {
    // Iterate service table, creating a thread for each:
    for(size_t i = 0; i &lt; COUNT_OF(FLIPPER_SERVICES); i++) {
        const FlipperInternalApplication* app = &amp;FLIPPER_SERVICES[i];
        FuriThread* thread = furi_thread_alloc_service(
            app-&gt;name, app-&gt;stack_size, app-&gt;app, NULL);
        furi_thread_set_appid(thread, app-&gt;appid);
        furi_thread_start(thread);
    }
}
</code></pre>
<p><strong>Why it matters</strong>: New services are added by declaring them in the table &#x2014; no factory code changes.</p>
<hr>
<h3 id="22-builder-pattern-two-phase-construction">2.2 Builder Pattern (Two-Phase Construction)</h3>
<p><strong>Concept</strong>: Object construction separated into allocation + configuration + start.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/thread.h</code>):</p>
<pre><code class="language-c">// Phase 1: Allocate with required params
FuriThread* thread = furi_thread_alloc_ex(&quot;MyApp&quot;, 2048, my_callback, ctx);

// Phase 2: Optional configuration
furi_thread_set_appid(thread, &quot;my_app&quot;);
furi_thread_set_priority(thread, FuriThreadPriorityHigh);

// Phase 3: Start
furi_thread_start(thread);
</code></pre>
<p>For services, a convenience <code>alloc_service()</code> variant skips the config phase (everything is fixed at compile time).</p>
<p><strong>Why it matters</strong>: Avoids constructors with 8 parameters while still catching misconfiguration at compile time.</p>
<hr>
<h3 id="23-singleton-via-record-registry">2.3 Singleton (via Record Registry)</h3>
<p><strong>Concept</strong>: Not your textbook Singleton. Uses a named global registry where services publish their interface pointer.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/record.h</code>):</p>
<pre><code class="language-c">// Publisher (during service init):
furi_record_create(RECORD_GUI, gui_instance);

// Consumer (anywhere in the system):
Gui* gui = furi_record_open(RECORD_GUI);
// ... use gui ...
furi_record_close(RECORD_GUI);

// Records are reference-counted:
// furi_record_open() increments holders_count
// furi_record_close() decrements it
// Record can only be destroyed when holders_count == 0
</code></pre>
<p><strong>Why this is better than a raw global</strong>:</p>
<ul>
<li>Reference counted (proper lifecycle tracking)</li>
<li>Thread-safe (mutex-guarded)</li>
<li>Blocking open: if a service hasn&apos;t created its record yet, the consumer waits on an event flag</li>
<li>Discoverable by name (no header dependency for every cross-module reference)</li>
</ul>
<p><strong>Lesson</strong>: Use a service locator with reference counting rather than bare <code>extern</code> globals.</p>
<hr>
<h2 id="3-structural-patterns">3. Structural Patterns</h2>
<h3 id="31-facade">3.1 Facade</h3>
<p><strong>Concept</strong>: A single header that provides unified access to a complex subsystem.</p>
<p><strong>Implementation</strong> (<code>lib/furi/furi.h</code>):</p>
<pre><code class="language-c">// A single #include gives access to the entire framework:
#include &lt;furi.h&gt;
// Now you have: furi_thread_*, furi_event_loop_*, furi_record_*, 
//               furi_pubsub_*, furi_log_*, FuriString, ...

// Similarly for HAL:
#include &lt;furi_hal.h&gt;
// Now you have: furi_hal_i2c_*, furi_hal_gpio_*, furi_hal_spi_*, ...
</code></pre>
<p><strong>Why it matters</strong>: New developers don&apos;t need to hunt through 30+ headers &#x2014; one include gets everything.</p>
<hr>
<h3 id="32-adapter-wrapper">3.2 Adapter / Wrapper</h3>
<p><strong>Concept</strong>: Wrapping a low-level API (FreeRTOS/Pico SDK) into a higher-level, safer, idiomatically consistent API.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/thread.c</code>):</p>
<pre><code class="language-c">// FuriThread wraps FreeRTOS TaskHandle_t:
struct FuriThread {
    StaticTask_t container;  // FreeRTOS TCB (at offset 0 for casting)
    char* name;
    char* appid;
    FuriThreadState state;
    FuriThreadCallback callback;
    void* context;
    // ... state management, heap tracking, stdio buffering, etc.
};

// The cast trick: FuriThread* can be used as TaskHandle_t
// because StaticTask_t is at offset 0:
static void furi_thread_body(void* context) {
    FuriThread* thread = (FuriThread*)context;
    // TLS setup, state transition, heap trace, then:
    thread-&gt;callback(thread-&gt;context);
    // cleanup, scrub enqueue, suspend
}
</code></pre>
<p><strong>Why it matters</strong>: The adapter adds:</p>
<ul>
<li>State machine (Stopped &#x2192; Starting &#x2192; Running &#x2192; Stopping &#x2192; Stopped)</li>
<li>Service thread protection (cannot be freed/joined)</li>
<li>Heap balance tracking</li>
<li>Thread-local storage association</li>
<li>All with the same FreeRTOS primitives underneath</li>
</ul>
<p><strong>Lesson</strong>: When a vendor SDK is awkward or unsafe, wrap it. Don&apos;t let FreeRTOS&apos; raw API leak into your application code.</p>
<hr>
<h3 id="33-opaque-pointer-pimpl">3.3 Opaque Pointer / PIMPL</h3>
<p><strong>Concept</strong>: The struct definition lives in the .c file (or <code>_i.h</code>). The public header only has a forward declaration.</p>
<p><strong>Implementation</strong>:</p>
<pre><code class="language-c">// In public header (bq25792.h):
typedef struct Bq25792 Bq25792;

Bq25792* bq25792_init(const FuriHalI2cBusHandle* handle, const GpioPin* pin_int);
Bq25792Status bq25792_enable_charging(Bq25792* instance);

// In private .c file:
struct Bq25792 {
    const FuriHalI2cBusHandle* i2c_handle;
    uint8_t address;
    const GpioPin* pin_interrupt;
    Bq25792CallbackInput callback;
    void* context;
};
</code></pre>
<p><strong>Why it matters</strong>:</p>
<ul>
<li>Consumers can&apos;t access internals (encapsulation enforced by the language)</li>
<li>The struct can change without recompiling consumers</li>
<li>No header dependency chain exposure</li>
</ul>
<p><strong>Lesson</strong>: Always make your driver instances opaque pointers. Consumers should never <code>sizeof(YourDriver)</code> or access fields directly.</p>
<hr>
<h3 id="34-strategy-pattern-via-callback-injection">3.4 Strategy Pattern (via Callback Injection)</h3>
<p><strong>Concept</strong>: Behavior is injected as function pointers rather than hardcoded.</p>
<p><strong>Implementation</strong> (<code>applications/services/gui/view.h</code>):</p>
<pre><code class="language-c">typedef void (*ViewLayoutCallback)(View* view, Canvas* canvas);
typedef bool (*ViewInputCallback)(View* view, const InputEvent* event);
typedef bool (*ViewInputTouchCallback)(View* view, const InputTouchEvent* event);

View* view_alloc(void);
void view_set_layout_callback(View* view, ViewLayoutCallback callback);
void view_set_input_callback(View* view, ViewInputCallback callback);
void view_set_input_touch_callback(View* view, ViewInputTouchCallback callback);

// Each application provides its own callbacks:
view_set_layout_callback(view, my_app_layout);
view_set_input_callback(view, my_app_input);
</code></pre>
<p>Also in <code>lib/furi/core/event_loop.h</code>:</p>
<pre><code class="language-c">// Event loop objects provide contracts via vtable:
typedef struct {
    const FuriEventLoopContractGetLink get_link;
    const FuriEventLoopContractGetLevel get_level;
} FuriEventLoopContract;

// Each subscribable type implements this differently:
extern const FuriEventLoopContract furi_event_loop_message_queue_contract;
extern const FuriEventLoopContract furi_event_loop_event_flag_contract;
</code></pre>
<p><strong>Why it matters</strong>: The GUI service has zero knowledge of what any specific app does &#x2014; it just calls the callbacks. New apps don&apos;t modify the GUI.</p>
<hr>
<h3 id="35-chain-of-responsibility">3.5 Chain of Responsibility</h3>
<p><strong>Concept</strong>: An event passes through a chain of handlers until one consumes it.</p>
<p><strong>Implementation</strong> (<code>applications/services/gui/gui.c</code>):</p>
<pre><code class="language-c">// Views are sorted by priority (Desktop &lt; Application &lt; Menu).
// Input routing walks from top priority downward:
static bool gui_input_events_glue_callback(void* context, const InputEvent* event) {
    Gui* gui = context;
    // Find highest-priority enabled view:
    View* top = gui_view_find_any_from_top(gui, view_is_visible);
    if(top) {
        // Try the view&apos;s input callback:
        if(view_input(top, event)) return true;  // Consumed!
        // If transparent and not consumed, try next view:
        if(view_is_transparent(top)) {
            View* next = gui_view_find_next_from_top(gui, view_is_visible, top);
            if(next) return view_input(next, event);
        }
    }
    return false;
}
</code></pre>
<p><strong>Lesson</strong>: For event handling in a layered UI, Chain of Responsibility is cleaner than switch statements or if-else chains.</p>
<hr>
<h2 id="4-behavioral-patterns">4. Behavioral Patterns</h2>
<h3 id="41-observer-pubsub">4.1 Observer / PubSub</h3>
<p><strong>Concept</strong>: One-to-many notification. Publishers don&apos;t know who&apos;s listening.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/pubsub.h</code>):</p>
<pre><code class="language-c">// Publisher side (input service):
FuriPubSub* event_pubsub = furi_pubsub_alloc();
furi_record_create(RECORD_INPUT_EVENTS, event_pubsub);

// When a button is pressed:
furi_pubsub_publish(event_pubsub, &amp;event);

// Subscriber side (GUI service):
FuriPubSub* input_pubsub = furi_record_open(RECORD_INPUT_EVENTS);
furi_pubsub_subscribe(input_pubsub, gui_input_events_glue_callback, gui);
</code></pre>
<p><strong>Why it matters</strong>: The input service doesn&apos;t know (or care) that GUI, I2C negotiator, and desktop are all listening. New listeners can be added without modifying the input service.</p>
<p><strong>Implementation caveat</strong>: The FURI PubSub is <strong>synchronous</strong> &#x2014; all callbacks fire inside the publish call, under the mutex. This is by design: it avoids queuing and is simple. But it means callbacks must be fast.</p>
<p><strong>Lesson</strong>: Use PubSub for one-to-many event distribution. Consider whether your system needs synchronous (simpler) or asynchronous (safer with slow handlers) delivery.</p>
<hr>
<h3 id="42-command-pattern-message-queue">4.2 Command Pattern (Message Queue)</h3>
<p><strong>Concept</strong>: Operations are packaged as messages and queued for serial processing by a dedicated thread.</p>
<p><strong>Implementation</strong> (<code>applications/services/power/power.c</code>):</p>
<pre><code class="language-c">// Each operation is a tagged message:
typedef enum {
    PowerMessageTypeIna219GetVoltage,
    PowerMessageTypeBq25792GetStatus,
    PowerMessageTypeBq25792SetChargeCurrent,
    // ...
} PowerMessageType;

typedef struct {
    PowerMessageType type;
    FuriApiLock* lock;  // NULL = fire-and-forget
    union {
        float voltage_v;           // result
        Bq25792Status charger_status; // result
        uint16_t charge_current_ma;   // parameter
        // ...
    } as;
} PowerMessage;

// Public API wraps message send:
bool power_bq25792_get_status(Bq25792Status* status) {
    PowerMessage msg = {
        .type = PowerMessageTypeBq25792GetStatus,
        .lock = api_lock_alloc_locked(),  // Future: wait for completion
        .as = {0}
    };
    power_send_message(msg);  // Enqueue + block on lock
    if(status) *status = msg.as.charger_status;
    api_lock_wait_unlock_and_free(msg.lock);
    return true;
}
</code></pre>
<p><strong>Why it matters</strong>:</p>
<ul>
<li>All hardware access happens on one thread (no concurrent register access)</li>
<li>Mutex-free driver design (serialized by queue)</li>
<li>ISR-safe: enqueue from interrupt context, process in thread context</li>
<li>Fire-and-forget vs synchronous-wait is caller&apos;s choice</li>
</ul>
<p><strong>Lesson</strong>: Any module that manages real hardware should use a command queue. This is the single most important pattern for thread-safe driver design.</p>
<hr>
<h3 id="43-proxy-synchronous-call-over-async-queue">4.3 Proxy (Synchronous Call over Async Queue)</h3>
<p><strong>Concept</strong>: The public API looks synchronous (returns a value), but internally it enqueues a message and blocks until the service thread processes it.</p>
<p><strong>Implementation</strong> (<code>applications/services/haptic/haptic.c</code>):</p>
<pre><code class="language-c">// The API wrapper macro:
#define API_WRAPPER(dev, message_type, result_expr) \
    if(!((instance)-&gt;devices &amp; (dev))) { break; } \
    PowerMessage msg = { .type = (message_type) }; \
    if(!power_send_message(instance, msg)) { break; } \
    (result_expr); \
    return true;

bool power_ina219_get_voltage_v(float* voltage) {
    API_WRAPPER_PARAM(
        PowerDeviceIna219,
        PowerMessageTypeIna219GetVoltage,
        { if(voltage) *voltage = msg.as.voltage_v; },
        msg.as.voltage_v = 0.0f
    );
    return false;
}
</code></pre>
<p><strong>The <code>FuriApiLock</code> implementation</strong> (<code>lib/toolbox/api_lock.h</code>):</p>
<pre><code class="language-c">typedef struct {
    FuriEventFlag* flag;
    uint32_t bit;
} FuriApiLock;

// Alloc locked (caller blocks immediately):
FuriApiLock* api_lock_alloc_locked(void) {
    FuriApiLock* lock = malloc(sizeof(FuriApiLock));
    lock-&gt;flag = furi_event_flag_alloc();
    lock-&gt;bit = 1;
    furi_event_flag_set(lock-&gt;flag, lock-&gt;bit);
    return lock;
}

// Service unlocks when done:
void api_lock_unlock(FuriApiLock* lock) {
    furi_event_flag_set(lock-&gt;flag, lock-&gt;bit);
}

// Caller waits:
void api_lock_wait_unlock_and_free(FuriApiLock* lock) {
    furi_event_flag_wait(lock-&gt;flag, lock-&gt;bit, FuriFlagWaitAny, FuriWaitForever);
    furi_event_flag_free(lock-&gt;flag);
    free(lock);
}
</code></pre>
<p><strong>Why it matters</strong>: The caller doesn&apos;t need to know about queues, threads, or async patterns. The API looks like a simple function call.</p>
<p><strong>Lesson</strong>: Hide your threading model behind synchronous-looking APIs. Let the implementation use message queues internally.</p>
<hr>
<h3 id="44-state-machine-explicit">4.4 State Machine (Explicit)</h3>
<p><strong>Concept</strong>: An enum of states + events that trigger transitions.</p>
<p><strong>Implementation</strong> (<code>applications/services/i2c_intercom/i2c_intercom.c</code>):</p>
<pre><code class="language-c">typedef enum {
    I2CIntercomStateIdle,
    I2CIntercomStateStart,
    I2CIntercomStateAddressSet,
    I2CIntercomStateAddressNoSet,
    I2CIntercomStateDataTransmitted,
} I2CIntercomState;

static void i2c_intercom_isr(FuriHalI2cBusHandle* handle, 
                              FuriHalI2cBusSlaveEvent event, void* context) {
    I2CIntercom* intercom = context;
    switch(event) {
    case FuriHalI2cBusSlaveEventStart:
        intercom-&gt;state = I2CIntercomStateStart;
        // Start timeout alarm
        break;
    case FuriHalI2cBusSlaveEventWrite:
        if(intercom-&gt;state == I2CIntercomStateStart) {
            intercom-&gt;addr_buf[intercom-&gt;addr_cnt++] = data;
            if(intercom-&gt;addr_cnt == 2) {
                intercom-&gt;state = I2CIntercomStateAddressSet;
                // Look up register, prepare for data
            }
        }
        break;
    case FuriHalI2cBusSlaveEventStop:
        intercom-&gt;state = I2CIntercomStateIdle;
        break;
    }
}
</code></pre>
<p><strong>Why it matters</strong>: The I2C protocol is inherently stateful (Start &#x2192; Address &#x2192; Data &#x2192; Stop). An explicit state machine makes the code provably correct.</p>
<p><strong>Lesson</strong>: When your protocol or peripheral has distinct phases, model it as an enum state machine. Don&apos;t use flags + if-else chains.</p>
<hr>
<h3 id="45-state-machine-counter-based">4.5 State Machine (Counter-Based)</h3>
<p><strong>Concept</strong>: State is tracked by integer counters with thresholds.</p>
<p><strong>Implementation</strong> (<code>applications/services/input/input.c</code>):</p>
<pre><code class="language-c">#define INPUT_DEBOUNCE_TICKS 4
#define INPUT_DEBOUNCE_TICKS_HALF (INPUT_DEBOUNCE_TICKS / 2)
#define INPUT_LONG_PRESS_COUNTS 25

typedef struct {
    volatile uint8_t debounce;       // 0-4 integration counter
    volatile uint8_t press_counter;  // 0-25+ press duration
    FuriTimer* press_timer;          // Periodic timer during press
    bool current_state;              // Current debounced state
} InputPinState;

// Debounce: integrate over multiple samples
void input_isr(void* context) {
    // Wakes the polling thread via thread flags
    furi_thread_flags_set(thread_id, INPUT_THREAD_FLAG_ISR);
}

void input_poll(Input* input) {
    for each pin {
        bool physical = read_gpio(pin);
        if(physical) {
            pin-&gt;debounce = MIN(pin-&gt;debounce + 1, INPUT_DEBOUNCE_TICKS);
        } else {
            pin-&gt;debounce = MAX(pin-&gt;debounce - 1, 0);
        }
        
        // Trigger on midpoint crossing:
        bool new_state = pin-&gt;debounce &gt; INPUT_DEBOUNCE_TICKS_HALF;
        if(new_state != pin-&gt;current_state) {
            pin-&gt;current_state = new_state;
            if(new_state) furi_timer_start(pin-&gt;press_timer, period);
            else furi_timer_stop(pin-&gt;press_timer);
        }
    }
}

// Timer callback: press duration tracking
void input_press_timer_callback(void* context) {
    InputPinState* pin = context;
    pin-&gt;press_counter++;
    if(pin-&gt;press_counter == INPUT_LONG_PRESS_COUNTS)
        emit(InputTypeLong);
    else if(pin-&gt;press_counter &gt; INPUT_LONG_PRESS_COUNTS)
        emit(InputTypeRepeat);
}
</code></pre>
<p><strong>Why it matters</strong>: This is a classic embedded debounce with integrated press-and-hold detection. No explicit state enum, but the behavior clearly has phases: idle &#x2192; press &#x2192; short &#x2192; long &#x2192; repeat.</p>
<hr>
<h3 id="46-template-method">4.6 Template Method</h3>
<p><strong>Concept</strong>: The skeleton of an algorithm is defined, with specific steps delegated to callbacks.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/thread.c</code>):</p>
<pre><code class="language-c">// The &quot;template&quot; &#x2014; fixed execution skeleton:
static void furi_thread_body(void* context) {
    FuriThread* thread = (FuriThread*)context;
    
    // 1. Set up thread-local storage
    vTaskSetThreadLocalStoragePointer(NULL, 0, thread);
    
    // 2. Transition state
    furi_thread_set_state(thread, FuriThreadStateRunning);
    
    // 3. Call the user&apos;s code (the &quot;hook&quot;)
    thread-&gt;callback(thread-&gt;context);
    
    // 4. Post-execution cleanup
    furi_thread_set_state(thread, FuriThreadStateStopping);
    
    // 5. Enqueue for scrubbing
    furi_message_queue_put(scrub_queue, &amp;thread, 0);
    
    // 6. Suspend self
    vTaskSuspend(NULL);
}
</code></pre>
<p><strong>Why it matters</strong>: Every thread gets consistent setup/teardown. Users can&apos;t forget to initialize TLS or clean up.</p>
<hr>
<h2 id="5-concurrency-patterns">5. Concurrency Patterns</h2>
<h3 id="51-actor-active-object">5.1 Actor / Active Object</h3>
<p><strong>Concept</strong>: Each service is an &quot;actor&quot; owning its thread, event loop, and message queue. External code communicates only via message passing.</p>
<p><strong>Implementation</strong>: Every service in <code>applications/services/</code> follows this:</p>
<pre><code class="language-c">// Every service looks like this:
typedef struct {
    FuriEventLoop* event_loop;    // Private event loop
    FuriMessageQueue* queue;      // Incoming command queue
    // driver instances owned by this service
    Bq25792* charger;
    INA219* current_sense;
} Power;

// Public API: enqueue messages
bool power_bq25792_get_status(Bq25792Status* status) {
    PowerMessage msg = { .type = PowerMessageTypeBq25792GetStatus, ... };
    furi_message_queue_put(power-&gt;queue, &amp;msg, timeout);
    // optionally block on API lock for result
}

// Private handler: process one message
static void power_queue_callback(void* context) {
    Power* power = context;
    PowerMessage msg;
    furi_message_queue_get(power-&gt;queue, &amp;msg, 0);
    switch(msg.type) {
        case PowerMessageTypeBq25792GetStatus:
            msg.as.charger_status = bq25792_get_status(power-&gt;charger);
            if(msg.lock) api_lock_unlock(msg.lock);
            break;
        // ...
    }
}
</code></pre>
<p><strong>Why it matters</strong>: No mutexes needed in the drivers &#x2014; all access is serialized by the queue. This is the single best pattern for multi-threaded embedded systems.</p>
<p><strong>Lesson</strong>: Design every module as an actor. Give it a queue. Make the public API just message construction + enqueue.</p>
<hr>
<h3 id="52-deferred-cleanup-scrub-queue">5.2 Deferred Cleanup (Scrub Queue)</h3>
<p><strong>Concept</strong>: A thread cannot safely delete itself. So it enqueues its own termination for another thread to process.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/thread.c</code>):</p>
<pre><code class="language-c">// Global scrub queue:
static FuriMessageQueue* furi_thread_scrub_message_queue = NULL;

// Thread body ends by enqueuing itself:
void furi_thread_body(void* context) {
    // ... user callback runs ...
    
    // Can&apos;t vTaskDelete() here (running on this stack!)
    furi_message_queue_put(furi_thread_scrub_message_queue, &amp;thread, 0);
    vTaskSuspend(NULL);
}

// Background scrubber processes dead threads:
void furi_background(void) {
    FuriThread* thread;
    while(furi_message_queue_get(furi_thread_scrub_message_queue, &amp;thread, timeout) == FuriStatusOk) {
        furi_thread_free(thread);  // Now safe to delete
    }
}
</code></pre>
<p>Similarly in the event loop: if you unsubscribe from within a callback, the item is marked for deferred free.</p>
<p><strong>Why it matters</strong>: You cannot <code>free()</code> a thread&apos;s stack from within that thread. The scrub queue pattern solves this cleanly.</p>
<hr>
<h3 id="53-insomnia-reference-counting">5.3 Insomnia Reference Counting</h3>
<p><strong>Concept</strong>: A counter prevents the system from sleeping while a peripheral is in use.</p>
<p><strong>Implementation</strong> (<code>targets/f100/furi_hal/furi_hal_power.c</code>):</p>
<pre><code class="language-c">static volatile FuriHalPower furi_hal_power = { .insomnia = 0 };

void furi_hal_power_insomnia_enter(void) {
    FURI_CRITICAL_ENTER();
    furi_check(furi_hal_power.insomnia &lt; UINT8_MAX);  // overflow guard
    furi_hal_power.insomnia++;
    FURI_CRITICAL_EXIT();
}

void furi_hal_power_insomnia_exit(void) {
    FURI_CRITICAL_ENTER();
    furi_check(furi_hal_power.insomnia &gt; 0);  // underflow guard
    furi_hal_power.insomnia--;
    FURI_CRITICAL_EXIT();
}

bool furi_hal_power_sleep_available(void) {
    return furi_hal_power.insomnia == 0;
}

// Called by I2C transactions:
void furi_hal_i2c_acquire(const FuriHalI2cBusHandle* handle) {
    furi_hal_power_insomnia_enter();  // Prevent sleep during transfer
    furi_mutex_acquire(bus-&gt;mutex, FuriWaitForever);
    // activate bus...
}

void furi_hal_i2c_release(const FuriHalI2cBusHandle* handle) {
    // deactivate bus...
    furi_mutex_release(bus-&gt;mutex);
    furi_hal_power_insomnia_exit();  // Now sleep is allowed
}
</code></pre>
<p><strong>Why it matters</strong>: An I2C transfer must not be interrupted by deep sleep. The reference count ensures sleep only happens when every peripheral agrees it&apos;s safe.</p>
<p><strong>Lesson</strong>: Use a reference-counted sleep veto pattern instead of ad-hoc sleep control.</p>
<hr>
<h3 id="54-two-level-error-handling">5.4 Two-Level Error Handling</h3>
<p><strong>Concept</strong>: Programming errors crash (assertions). Runtime errors return codes.</p>
<table>
<thead>
<tr>
<th>Error Type</th>
<th>Mechanism</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Null pointer</td>
<td><code>furi_check(ptr)</code> &#x2192; crash</td>
<td>Calling with NULL handle</td>
</tr>
<tr>
<td>Wrong thread</td>
<td><code>furi_check(owner == current)</code> &#x2192; crash</td>
<td>Calling I2C without acquire</td>
</tr>
<tr>
<td>Invalid config</td>
<td><code>furi_crash()</code> &#x2192; crash</td>
<td>SPI mode = 5</td>
</tr>
<tr>
<td>I2C timeout</td>
<td>Returns <code>PICO_ERROR_TIMEOUT</code></td>
<td>Bus locked up</td>
</tr>
<tr>
<td>Charger fault</td>
<td>Returns <code>Bq25792StatusError</code></td>
<td>Overcurrent</td>
</tr>
</tbody>
</table>
<p>Implementation:</p>
<pre><code class="language-c">// Programming errors = crash (contract violation):
void furi_hal_i2c_acquire(const FuriHalI2cBusHandle* handle) {
    furi_check(handle);
    furi_check(handle-&gt;bus);
    furi_check(handle-&gt;bus-&gt;current_handle == NULL || 
               handle-&gt;bus-&gt;current_handle == handle);
    // ...
}

// Runtime errors = return code:
Bq25792Status bq25792_read_register(Bq25792* inst, uint8_t reg, uint8_t* data) {
    furi_check(inst);   // Programming error &#x2192; crash
    furi_hal_i2c_acquire(inst-&gt;i2c_handle);
    int ret = furi_hal_i2c_master_tx_blocking(inst-&gt;i2c_handle, addr, &amp;reg, 1, timeout);
    furi_hal_i2c_release(inst-&gt;i2c_handle);
    if(ret != PICO_OK) return Bq25792StatusError;  // Runtime error &#x2192; return
    return Bq25792StatusOk;
}
</code></pre>
<p><strong>Why it matters</strong>: You catch bugs fast (crash = immediate attention) while handling expected failures gracefully (timeout = retry).</p>
<hr>
<h2 id="6-embedded-specific-patterns">6. Embedded-Specific Patterns</h2>
<h3 id="61-memory-mapped-register-emulation-software-mmio">6.1 Memory-Mapped Register Emulation (Software MMIO)</h3>
<p><strong>Concept</strong>: Emulate a hardware register file over I2C, with callbacks on write.</p>
<p><strong>Implementation</strong> (<code>applications/services/i2c_intercom/i2c_registers.c</code>):</p>
<pre><code class="language-c">// Register definition:
typedef struct {
    uint16_t address;
    uint16_t value;
    I2CRegisterFlag flags;     // R, W, RC (read-to-clear)
    I2CRegisterCallback callback;  // Fires on write
    void* context;
} I2CReg;

// Register map (hash table):
static I2CRegMap_t reg_map;  // I2CReg address &#x2192; I2CReg

void i2c_register_set(I2CRegisters* i2c, uint16_t address, uint16_t value) {
    with_i2c_register({
        I2CReg* reg = i2c_register_get(address);
        if(reg &amp;&amp; (reg-&gt;flags &amp; I2CRegisterFlagW)) {
            reg-&gt;value = value;
            if(reg-&gt;callback) reg-&gt;callback(reg-&gt;context);
        }
    });
}
</code></pre>
<p><strong>Register map</strong> (<code>i2c_registers_map.h</code>):</p>
<pre><code class="language-c">// The CPU sees this layout over I2C:
#define I2C_REG_STATUS          0x0000  // MCU status
#define I2C_REG_INPUT_EVENTS    0x0002  // Button state bitmask
#define I2C_REG_TOUCH_X         0x0004  // Touchpad X
#define I2C_REG_TOUCH_Y         0x0006  // Touchpad Y
#define I2C_REG_TOUCH_PRESSURE  0x0008  // Touchpad pressure
#define I2C_REG_HEADPHONES      0x000A  // Headphone status
#define I2C_REG_LED_BRIGHTNESS  0x000C  // LED brightness
#define I2C_REG_LED_COLOR       0x000E  // LED color
</code></pre>
<p>With interrupt signaling:</p>
<pre><code class="language-c">void i2c_registers_set_interrupt(I2CRegisters* i2c, uint16_t addr, uint16_t bit) {
    with_i2c_register({
        I2CReg* ireg = i2c_register_get(addr);
        if(ireg) {
            ireg-&gt;value |= bit;
            // Check mask: if corresponding mask bit is 0, assert interrupt
            I2CReg* mask = i2c_register_get(I2C_REG_INTERRUPT_MASK);
            if(mask &amp;&amp; !(mask-&gt;value &amp; bit)) {
                furi_hal_gpio_write(&amp;gpio_cpu_int, false);  // INT low = IRQ
            }
        }
    });
}
</code></pre>
<p><strong>Why it matters</strong>: This is how an MCU communicates with a main CPU over I2C without a custom protocol. The CPU just reads/writes &quot;registers&quot; like any memory-mapped peripheral.</p>
<p><strong>Lesson</strong>: When designing inter-processor communication, define a register map. It&apos;s simpler and more debuggable than a message-based protocol.</p>
<hr>
<h3 id="62-gpio-interrupt-registry">6.2 GPIO Interrupt Registry</h3>
<p><strong>Concept</strong>: A centralized array mapping pin numbers to callbacks, with critical-section protection.</p>
<p><strong>Implementation</strong> (<code>targets/f100/furi_hal/furi_hal_gpio.c</code>):</p>
<pre><code class="language-c">#define GPIO_NUMBER 48

typedef struct {
    GpioExtiCallback callback;
    void* context;
    GpioCondition condition;  // Rise, Fall, or Both
} GpioInterrupt;

static volatile GpioInterrupt gpio_interrupt[GPIO_NUMBER];

void furi_hal_gpio_add_int_callback(const GpioPin* gpio, 
                                     GpioExtiCallback cb, void* ctx) {
    FURI_CRITICAL_ENTER();
    furi_check(gpio-&gt;pin &lt; GPIO_NUMBER);
    furi_check(gpio_interrupt[gpio-&gt;pin].callback == NULL);  // No double-reg
    gpio_interrupt[gpio-&gt;pin].callback = cb;
    gpio_interrupt[gpio-&gt;pin].context = ctx;
    FURI_CRITICAL_EXIT();
}

// Single ISR handler dispatches to registered callbacks:
void gpio_irq_handler(void) {
    uint32_t event = gpio_get_irq_event_mask(pin);
    uint idx = pin;
    GpioInterrupt* gi = &amp;gpio_interrupt[idx];
    if(gi-&gt;callback) {
        gi-&gt;callback(gi-&gt;context, idx);
    }
}
</code></pre>
<p><strong>Why it matters</strong>: Centralized interrupt management prevents conflicts, enables debug, and provides a consistent API.</p>
<hr>
<h3 id="63-vtable-based-polymorphism">6.3 Vtable-based Polymorphism</h3>
<p><strong>Concept</strong>: C function pointer tables for runtime polymorphism, without C++.</p>
<p><strong>Implementation</strong> (<code>targets/f100/furi_hal/furi_hal_i2c.c</code>):</p>
<pre><code class="language-c">// Each I2C bus implementation provides these operations:
typedef union {
    struct {
        FuriHalI2cBusEventCallback event;
    } master;
    struct {
        FuriHalI2cBusSlaveEventCallback event;
    } slave;
} FuriHalI2cBusAPI;

// The bus struct uses the API union:
struct FuriHalI2cBus {
    void* data;                    // PIO instance or HW I2C peripheral
    FuriMutex* mutex;
    FuriHalI2cBusAPI api;
    FuriHalI2cMode mode;
    const FuriHalI2cBusHandle* current_handle;
};

// Three buses exist, each with different implementation:
// - Control bus: PIO-based I2C on GPIO20/21 (400kHz)
// - Main bus: PIO-based I2C on GPIO22/23 (400kHz)  
// - CPU bus: Hardware I2C1 on GPIO10/11 (100kHz, slave-capable)
</code></pre>
<p><strong>Why it matters</strong>: The I2C abstraction supports both PIO-based and hardware I2C with the same API. No conditional compilation at the call site.</p>
<hr>
<h3 id="64-isr-safe-dual-path-apis">6.4 ISR-Safe Dual-Path APIs</h3>
<p><strong>Concept</strong>: Functions that can be called from both task and ISR context check the mode and use the correct FreeRTOS API variant.</p>
<p><strong>Implementation</strong> (scattered throughout the codebase):</p>
<pre><code class="language-c">void furi_thread_flags_set(FuriThread* thread, uint32_t flags) {
    if(FURI_IS_IRQ_MODE()) {
        // FromISR variant (no context switch):
        xTaskNotifyFromISR(thread-&gt;container, flags, eSetBits, NULL);
    } else {
        // Task variant (can context switch):
        xTaskNotify(thread-&gt;container, flags, eSetBits);
    }
}
</code></pre>
<p><strong>Why it matters</strong>: Same function works in both contexts. The caller doesn&apos;t need to know or care.</p>
<hr>
<h3 id="65-c11-generic-for-function-overloading">6.5 C11 <code>_Generic</code> for Function Overloading</h3>
<p><strong>Concept</strong>: C11&apos;s <code>_Generic</code> keyword dispatches to different implementations based on argument type.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/string.h</code>):</p>
<pre><code class="language-c">// &quot;Overloaded&quot; set: works with both FuriString* and const char*:
#define furi_string_set(a, b) \
    _Generic((b), \
        FuriString*: furi_string_set, \
        const char*: furi_string_set_str, \
        char*: furi_string_set_str \
    )(a, b)

// Usage:
FuriString* s = furi_string_alloc();
furi_string_set(s, other_string);  // calls furi_string_set()
furi_string_set(s, &quot;hello&quot;);       // calls furi_string_set_str()
</code></pre>
<p><strong>Why it matters</strong>: Cleaner API without printf-style format strings. Type-safe at compile time.</p>
<hr>
<h3 id="66-pool-allocation-for-critical-paths">6.6 Pool Allocation for Critical Paths</h3>
<p><strong>Concept</strong>: Pre-allocate memory from a fixed pool at init time for threads that must never fail allocation.</p>
<p><strong>Implementation</strong> (<code>lib/furi/core/thread.c</code>):</p>
<pre><code class="language-c">FuriThread* furi_thread_alloc_service(const char* name, 
                                       uint32_t stack_size,
                                       FuriThreadCallback callback, 
                                       void* context) {
    // Allocate from pool &#x2014; guaranteed to succeed:
    FuriThread* thread = memmgr_alloc_from_pool(sizeof(FuriThread));
    thread-&gt;stack = memmgr_alloc_from_pool(stack_size);
    // Can&apos;t be freed, can&apos;t be joined, can&apos;t be modified
    thread-&gt;is_service = true;
    return thread;
}
</code></pre>
<p><strong>Why it matters</strong>: Service threads must never fail to allocate, even if the heap is fragmented. Pool allocation guarantees this.</p>
<hr>
<h2 id="7-pattern-usage-map">7. Pattern Usage Map</h2>
<table>
<thead>
<tr>
<th>Pattern</th>
<th>Module</th>
<th>What For</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Microkernel</strong></td>
<td><code>applications/applications.h</code></td>
<td>Service/plugin registry</td>
</tr>
<tr>
<td><strong>Reactor</strong></td>
<td><code>lib/furi/core/event_loop.*</code></td>
<td>Central event dispatch</td>
</tr>
<tr>
<td><strong>Facade</strong></td>
<td><code>lib/furi/furi.h</code>, <code>furi_hal.h</code></td>
<td>Single-include API surface</td>
</tr>
<tr>
<td><strong>Adapter</strong></td>
<td><code>lib/furi/core/thread.*</code></td>
<td>Wrapping FreeRTOS tasks</td>
</tr>
<tr>
<td><strong>Opaque Pointer</strong></td>
<td>All drivers (bq25792, drv2605l, etc.)</td>
<td>Encapsulation</td>
</tr>
<tr>
<td><strong>Strategy (callback)</strong></td>
<td><code>gui/view.*</code>, <code>event_loop.*</code></td>
<td>Injected behavior</td>
</tr>
<tr>
<td><strong>Observer</strong></td>
<td><code>lib/furi/core/pubsub.*</code></td>
<td>Input events, power events</td>
</tr>
<tr>
<td><strong>Command</strong></td>
<td><code>services/power/*</code>, <code>services/haptic/*</code></td>
<td>Typed message queues</td>
</tr>
<tr>
<td><strong>Proxy</strong></td>
<td><code>services/power/*</code></td>
<td>Sync API over async queue</td>
</tr>
<tr>
<td><strong>Chain of Resp.</strong></td>
<td><code>services/gui/gui.c</code></td>
<td>Input routing through views</td>
</tr>
<tr>
<td><strong>State Machine</strong></td>
<td><code>services/input/*</code>, <code>i2c_intercom/*</code></td>
<td>Debounce + I2C protocol</td>
</tr>
<tr>
<td><strong>Template Method</strong></td>
<td><code>lib/furi/core/thread.*</code></td>
<td>Thread body skeleton</td>
</tr>
<tr>
<td><strong>Singleton</strong></td>
<td><code>lib/furi/core/record.*</code></td>
<td>Service locator</td>
</tr>
<tr>
<td><strong>Builder</strong></td>
<td><code>lib/furi/core/thread.*</code></td>
<td>Two-phase thread construction</td>
</tr>
<tr>
<td><strong>Factory</strong></td>
<td><code>lib/furi/flipper.c</code></td>
<td>Thread creation from table</td>
</tr>
<tr>
<td><strong>Actor</strong></td>
<td>All services</td>
<td>Message-queue-per-thread</td>
</tr>
<tr>
<td><strong>Scrub Queue</strong></td>
<td><code>lib/furi/core/thread.*</code></td>
<td>Deferred thread cleanup</td>
</tr>
<tr>
<td><strong>Ref. Counting</strong></td>
<td><code>furi_hal_power.*</code></td>
<td>Sleep veto</td>
</tr>
<tr>
<td><strong>Software MMIO</strong></td>
<td><code>i2c_intercom/*</code></td>
<td>Virtual register map</td>
</tr>
<tr>
<td><strong>Pool Allocator</strong></td>
<td><code>lib/furi/core/thread.*</code></td>
<td>Guaranteed service allocation</td>
</tr>
<tr>
<td><strong>PIMPL</strong></td>
<td><code>lib/furi/core/string.*</code></td>
<td>FuriString opacity</td>
</tr>
<tr>
<td><strong>Vtable</strong></td>
<td><code>furi_hal_i2c.*</code></td>
<td>Polymorphic I2C backends</td>
</tr>
<tr>
<td><strong>Fail-Fast</strong></td>
<td>Everywhere with <code>furi_check()</code></td>
<td>Contract enforcement</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="8-key-takeaways-for-your-embedded-designs">8. Key Takeaways for Your Embedded Designs</h2>
<h3 id="do-this">Do This:</h3>
<ol>
<li>
<p><strong>Use a reactor (event loop) everywhere</strong> &#x2014; never write a custom polling loop. Every module gets a <code>FuriEventLoop</code>-like construct.</p>
</li>
<li>
<p><strong>Make every driver instance an opaque pointer</strong> &#x2014; consumers should never see your struct internals.</p>
</li>
<li>
<p><strong>Use typed message queues for hardware access</strong> &#x2014; serialize all peripheral access through one thread. It eliminates most concurrency bugs.</p>
</li>
<li>
<p><strong>Use a service locator (not globals)</strong> &#x2014; give each module a named record. Reference-count it. Make <code>open()</code> block until the service is ready.</p>
</li>
<li>
<p><strong>Use fail-fast assertions for contract violations</strong> &#x2014; null pointers, invalid state, wrong-thread access should crash immediately. Don&apos;t return error codes for programmer mistakes.</p>
</li>
<li>
<p><strong>Define a register map for inter-processor comms</strong> &#x2014; it&apos;s simpler and more debuggable than a custom message protocol.</p>
</li>
<li>
<p><strong>Use a sleep veto reference count</strong> &#x2014; peripherals increment it during transactions, the idle loop checks it before sleeping.</p>
</li>
<li>
<p><strong>Build a central service table</strong> &#x2014; all tasks declared in one place. Adding a feature = adding one table entry.</p>
</li>
<li>
<p><strong>Use callback-based injection</strong> &#x2014; your GUI/service should call function pointers, not know about specific implementations.</p>
</li>
<li>
<p><strong>Defer cleanup that can&apos;t happen in context</strong> &#x2014; use a scrub queue for thread deletion, mark-for-defer for unsubscription during callback.</p>
</li>
</ol>
<h3 id="avoid-this">Avoid This:</h3>
<ul>
<li>Raw <code>extern</code> globals for inter-module access</li>
<li>Custom polling loops</li>
<li>Driver functions with no error return (but also don&apos;t return error codes for null pointers &#x2014; assert those)</li>
<li>Shared mutable state without serialization</li>
<li>Hard-coded thread priorities scattered across files</li>
<li>Direct FreeRTOS API calls in application code</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Digital Filter Design in 30 minutes]]></title><description><![CDATA[<p>Lately, I&apos;m re-introducing myself to digital signal processing after the course I took in university. Back in the days, I performed really badly and even passed the course with a grade just above the threshold. Don&apos;t get me wrong, I loved the topic and its real-life</p>]]></description><link>https://gyokhan.com/digital-filter-design-in-30-minutes/</link><guid isPermaLink="false">69d0c9c71460630001eb6096</guid><category><![CDATA[digital-signal-processing]]></category><category><![CDATA[control theory]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Mon, 06 Apr 2026 13:32:01 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1603619890744-ed995cb96219?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fG11c2ljJTIwZXF8ZW58MHx8fHwxNzc1NDgyMjY4fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1603619890744-ed995cb96219?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fG11c2ljJTIwZXF8ZW58MHx8fHwxNzc1NDgyMjY4fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="Digital Filter Design in 30 minutes"><p>Lately, I&apos;m re-introducing myself to digital signal processing after the course I took in university. Back in the days, I performed really badly and even passed the course with a grade just above the threshold. Don&apos;t get me wrong, I loved the topic and its real-life applications. However, due to errands in my life, it became really hard to study for the course.</p><p>Two weeks ago, I decided to learn it again. Thanks to all the free time I have now, with AI-guided learning, I can learn it as fast as I can, with a focus on a real-life problem to understand the edge cases in use. Therefore, I started to implement a digital signal processing library in a programming language from scratch. Function generators, FFTs/iFFTs, windowing, plotting functions, etc., are already done, and the next step was to realise a digital filter. I asked for a resource on Reddit for a short and &quot;enough&quot; to understand the basics. The ones suggested weren&apos;t that great. Then, I did a guided learning session with LLMs, experiment what I learned using <code>scipy</code> in Python and said why not make the resource myself for the future learners.</p><p>This guide will be a story-like introduction to digital filter design. I write it for an audience that has minimal background in signal analysis.</p><h3 id="signals-and-their-representation">Signals and Their Representation</h3><p>I&apos;m a simple guy, and for me, a signal is an information flow that changes over time or in any other medium. A sound, an image, a video, all of them are signals in my perception. From time to time, we need to modify the signals, either to reduce the noise or apply cool guitar effects. This is where signal processing experts come in and try to achieve the optimal system for their problem.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-671b0517-037c-4373-9964-36f20c50e158.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1034" height="547" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-671b0517-037c-4373-9964-36f20c50e158.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-671b0517-037c-4373-9964-36f20c50e158.png 1000w, https://gyokhan.com/content/images/2026/04/data-src-image-671b0517-037c-4373-9964-36f20c50e158.png 1034w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">This is a sample signal for a recording I had with &quot;Baglama&quot;, a Turkish instrument. It is in time-domain where you can see the amplitude of the signal on Y-axis and Time in X-axis.</span></figcaption></figure><p>A analgoue signal is a continuous wave, which means it has no &quot;samples&quot; and has infinite resolution. However, in the computation domain, we are required to have samples since we can not store an infinite number of data points. We select a <strong>sample rate</strong> to only store a dedicated number of samples per second.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-e83963a2-842f-40f8-9eac-e330e4c96a0e.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1019" height="547" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-e83963a2-842f-40f8-9eac-e330e4c96a0e.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-e83963a2-842f-40f8-9eac-e330e4c96a0e.png 1000w, https://gyokhan.com/content/images/2026/04/data-src-image-e83963a2-842f-40f8-9eac-e330e4c96a0e.png 1019w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">This is the sample sample signal for the same recording, but I just took the time until 0.0065 to zoom into details.</span></figcaption></figure><p>In the figure above, you can see what a digital signal actually looks like. Only the samples are stored, and the non-stored ranges are calculated using interpolation. When we have a high number of samples per second, it&apos;s easy to construct the continuous signal back using interpolation. However, this is not the case most of the time. For a set of data points, we can have a different signal reconstruction. Find an example below where we have three samples, and three waves can be constructed.</p><figure class="kg-card kg-image-card"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-21d99a69-7357-4cbe-95d1-9ebb044a1081.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1012" height="624" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-21d99a69-7357-4cbe-95d1-9ebb044a1081.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-21d99a69-7357-4cbe-95d1-9ebb044a1081.png 1000w, https://gyokhan.com/content/images/2026/04/data-src-image-21d99a69-7357-4cbe-95d1-9ebb044a1081.png 1012w" sizes="(min-width: 720px) 720px"></figure><p>This is called aliasing. It is the problem of reconstructing N samples to correct continues wave. Luckily, we know how to solve it perfectly &#x2013; at least up to a level. There&apos;s a <strong>Nyquist rate</strong> definition, and it says that a signal can be constructed perfectly if the highest frequency in the signal composition is below half of the sample rate.</p><p>So, for a sound signal (which is nearly always 44100 samples in our daily life), we can use the Nyquist rate and say that we can construct the signal from data samples for frequencies from 0 Hz to 22050 Hz.</p><p>One may ask, &quot;What does it mean to have frequencies in a signal? Isn&apos;t it all random? Where do we see the periods?&quot;. Let&apos;s answer this question in the next section.</p><h3 id="frequency-domain">Frequency Domain</h3><p>Normally, we express a signal in the time domain. Where a signal $y(t)$s values are dependent on $t$, a time variable. Although this is more intuitive for a general audience, the disadvantage is that it hides patterns of a signal.</p><p>A signal, even though it looks random, can be expressed using its patterns. Let&apos;s look at an example. Imagine having three sine signals with different frequencies. When we add their $y_n(t)$ values for every $t$, we&apos;ll see a signal that is a combination of all three and follows their characteristics. Find an example in the figure below.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-fda6705c-eb14-4cc9-a035-7e0ca8db4399.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1389" height="1025" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-fda6705c-eb14-4cc9-a035-7e0ca8db4399.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-fda6705c-eb14-4cc9-a035-7e0ca8db4399.png 1000w, https://gyokhan.com/content/images/2026/04/data-src-image-fda6705c-eb14-4cc9-a035-7e0ca8db4399.png 1389w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The last plot is first three&apos;s addition. See how high frequency (third wave) affected the last graph and how first wave influence the general wave shape.</span></figcaption></figure><p>As you can see from the last figure&apos;s last plot, although a signal looks kinda random, it has periodic signals, or in my call, patterns inside. I know that some of you would still find the last one an easy-to-say example. Therefore, I generated the following figure, which is the combination of 30 perfect sine waves. If someone would show me this figure, I&apos;d definitely say that it&apos;s all random!</p><figure class="kg-card kg-image-card"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-26661f94-2550-4d25-85bd-843a53e11036.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1167" height="547" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-26661f94-2550-4d25-85bd-843a53e11036.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-26661f94-2550-4d25-85bd-843a53e11036.png 1000w, https://gyokhan.com/content/images/2026/04/data-src-image-26661f94-2550-4d25-85bd-843a53e11036.png 1167w" sizes="(min-width: 720px) 720px"></figure><p>Back in time, Fourier found a way to decompose a random-looking signal into its patterns. It&apos;s called the Fourier Transform. It gets a function $f(t)$ and converts it to $F(w)$ where $w$ is the angular frequency (radians/second). Although the equation is super important, for the sake of the current getting-started vibe, I&apos;m not going to go into the depths of the equation. Please find <a href="https://betterexplained.com/articles/an-interactive-guide-to-the-fourier-transform/" rel="noreferrer">this website</a> for more details.</p><p><strong>Fourier Transform (from time domain to frequency domain):</strong></p><p>$$<br>F(\omega) = \int_{-\infty}^{\infty} f(t) e^{-j\omega t} dt<br>$$</p><p><strong>Inverse Fourier Transform (from frequency domain to time domain):</strong></p><p>$$<br>f(t) = \frac{1}{2\pi} \int_{-\infty}^{\infty} F(\omega) e^{j\omega t} d\omega<br>$$</p><p>By using the Fourier Transform on a quite random-looking signal, for example, the figure above with 30 frequencies, we can gather the frequency elements one by one with their phase and amplitude information. This is quite useful for signal processing, since we&apos;re mainly interested in a range of frequencies, and others can be ignored.</p><p>Imagine you capture a voice memo from your class. However, after you listened to the recording, you realised that there&apos;s a constant background noise. What you want to do is to cancel this noise somehow without touching the professor&apos;s voice. First of all, you&apos;ll apply the Fourier Transform to the recording to see what frequencies are there and at high amplitude. Then, you&apos;re going to find the frequency range your professor speaks &#x2013; this will be mainly experimentation with different frequencies and listening to them. When you find that your professor&apos;s voice is between $[f_1, f_2]$ Hz, you&apos;re going to remove all other frequency components in the Fourier Transform&apos;s output, which are $(0, f_1)$ and $(f_2, \infty)$.  When you have the sine wave composition for the frequencies only between $f_1$ and $f_2$, you&apos;ll apply the Inverse Fourier Transform to get the time domain back, and listen to a recording that has no environmental noise! This is actually a simple explanation of how an equaliser works.</p><p>As you see, what Fourier achieves is quite remarkable. That&apos;s why it&apos;s also selected as one of the most important 10 equations by an IEEE journal that I cannot recall now. In signal processing, we can work with any domain we think is best, and frequency is one of the most popular ones for many problems.</p><p>In the digital domain, we cannot calculate the integrals for a continuous signal. Therefore, there was a need to modify the original Fourier Transform to work with discrete/digital samples. In the following equations, you can see the modified version. The logic and the background are all the same, but this one is more useful for us in digital signal processing. Please find t<a href="https://ocw.mit.edu/courses/res-6-007-signals-and-systems-spring-2011/9434f9bde960fd3b2b81de3ad442f902_MITRES_6_007S11_lec11.pdf" rel="noreferrer">his nice chapter from Oppenheim&apos;s Signals and Systems textbook</a> on how to derive the discrete-time from the original equation.</p><p><strong>Discrete-Time Fourier Transform (from time domain to frequency domain):</strong></p><p>$$<br>X(\omega) = \sum_{n=-\infty}^{\infty} x[n] e^{-j\omega n}<br>$$</p><p><strong>Inverse Discrete-Time Fourier Transform (from frequency domain to time domain):</strong></p><p>$$<br>x[n] = \frac{1}{2\pi} \int_{-\pi}^{\pi} X(\omega) e^{j\omega n} d\omega<br>$$</p><h3 id="z-transform">Z-Transform</h3><p>The problem with the Fourier Transform is its nature, which only allows us to use it for well-behaved signals. You can use it for stable signals (do not expand to infinity in amplitude) or non-causal ones (no start or end time of the signal). As you can see, it is useful for a specific part of the world; however, the world is not that simple.</p><p>In real life, our signals have a starting point. From time to time, we see them being amplified to infinity. Nothing is perfect, and we need to have a function to generate the frequency domain for these signals as well. </p><p>Here&apos;s the generalised version of the (Discrete-Time) Fourier Transform. It is called the Z-transform.</p><p>$$<br>X(z) = \sum_{n=-\infty}^{\infty} x[n] z^{-n}<br>$$</p><p>where $z = re^{j\omega}$. Let&apos;s substitute the z with its definition to see what it looks like using $\omega$</p><p>$$<br>X(e^{j\omega}) = \sum_{n=-\infty}^{\infty} x[n] (re^{j\omega})^{-n}<br>$$</p><p>Please notice the difference between the Discrete-Time Fourier Transform and the Z-Transform. It is the only $r$ multiplier factor. This is called the <strong>damping</strong> <strong>factor</strong>. It tells us if this frequency will stay the same ($r=1$), grow to infinity ($r&gt;1$) or decay to zero ($r&lt;1$).</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-9a0772b3-e3b6-4d0e-b2db-bf1cd5daaf39.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1390" height="1229" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-9a0772b3-e3b6-4d0e-b2db-bf1cd5daaf39.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-9a0772b3-e3b6-4d0e-b2db-bf1cd5daaf39.png 1000w, https://gyokhan.com/content/images/2026/04/data-src-image-9a0772b3-e3b6-4d0e-b2db-bf1cd5daaf39.png 1390w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Three dumping factors for 8 Hz sine wave. Please notice that for r &gt; 1, the signal grows to infinity.</span></figcaption></figure><p>As you can suspect, the Fourier Transform is the special case of the Z-Transform for $r=1$, and that&apos;s why we cannot use it for signals decaying or growing.</p><p>The best part of the Z-Transform is to easily describes the time delays with a power of $z$. It&apos;s called the <strong>time</strong> <strong>shifting property</strong>,<strong> </strong>and through the guide, we&apos;ll use this property to express the delays in our signals.</p><p>Let&apos;s imagine shifting our sample by $k$,</p><p>$$<br>Y_{n}(z) = \sum_{n=-\infty}^{\infty} x[n-k] z^{-n}<br>$$</p><p>To simplify this, we perform a change of variables. Let $m=n&#x2212;k$, which implies $n=m+k$. As $n$ goes from $&#x2212;\infty$ to $\infty$, $m$ does as well.</p><p>$$<br>Y_{n}(z) = \sum_{m=-\infty}^{\infty} x[m] z^{-(m+k)}<br>$$</p><p>We can pull the constant $z^{&#x2212;k}$ out of the summation because it does not depend on $m$:</p><p>$$<br>Y_{n}(z) = z^{-k} \sum_{m=-\infty}^{\infty} x[m] z^{-m}<br>$$</p><p>As we already defined the summation part as the Z-transform of a signal, we can substitute our $Y_{n-k}(z)$ there. </p><p>$$<br>Y_{n}(z) = z^{-k} Y_{n-k}(z)<br>$$</p><p>This derivation proves to us that a signal shifted by $k$ samples to the back (due to the minus), in Z-domain, corresponds to a multiplication with $z^{-k}$. We&apos;ll use this property in the next section to provide a foundation for how we can write a signal $Y(z)$ based on the inputs and outputs of a system.</p><h3 id="transfer-function-of-a-system">Transfer Function of a System</h3><p>Every system is a black box. There are inputs and output pairs, and a mapping from inputs to outputs. This mapping can be a polynomial, a differential equation, a machine, a web form, a neural network, or a super-weird alien technology. Most of the time, we design our systems based on the requirements that define the inputs we can get and the outputs we should produce. This is called model-driven design.</p><p>When a model-driven design is completed, the system is given to an engineer with field information. For example, a model that converts 12 Volts to 5 Volts is given to electrical engineers, and they realise the model within a circuit design. Another example, a model that suppresses particular vibrations in a machine is given to a mechanical engineer, and they realise the model within correct material selection and mechanical placements. This is how I tend to work in my software design process as well. I know which inputs are given to me, and what outputs are expected from them, and prepare an algorithm that will realise it in your device.</p><p>Therefore, until the white box is solved (this is how QA engineers call a system where the inner logic is known), we have to find a mapping for our black box. This mapping is called a <strong>transfer function </strong>(or <strong>system function</strong>), and is defined as the ratio of outputs to inputs in complex variables.</p><p><strong>Discrete-Time Transfer Function</strong></p><p>$$<br>H(z) = \frac{Y(z)}{X(z)}<br>$$</p><p>It&apos;s continues-time form is constructed with the Laplace Transform, which is not in the scope of the current guide.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-f4d9c321-843a-45c1-ae0b-d8660388f455.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1908" height="408" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-f4d9c321-843a-45c1-ae0b-d8660388f455.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-f4d9c321-843a-45c1-ae0b-d8660388f455.png 1000w, https://gyokhan.com/content/images/size/w1600/2026/04/data-src-image-f4d9c321-843a-45c1-ae0b-d8660388f455.png 1600w, https://gyokhan.com/content/images/2026/04/data-src-image-f4d9c321-843a-45c1-ae0b-d8660388f455.png 1908w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A simple block diagram that shows what transfer function is and how to interpret black-box design.</span></figcaption></figure><p>The best part of the transfer functions is their ability to cascade. It means that one can place $H_1(z)$, $H_2(z)$, $H_3(z)$, and many more next to each other where each ones outptus will be an input to the next one to represent a high-order system.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-bf38e9f0-f0c1-4c7e-b302-c2fe219aeb51.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="2000" height="388" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-bf38e9f0-f0c1-4c7e-b302-c2fe219aeb51.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-bf38e9f0-f0c1-4c7e-b302-c2fe219aeb51.png 1000w, https://gyokhan.com/content/images/size/w1600/2026/04/data-src-image-bf38e9f0-f0c1-4c7e-b302-c2fe219aeb51.png 1600w, https://gyokhan.com/content/images/size/w2400/2026/04/data-src-image-bf38e9f0-f0c1-4c7e-b302-c2fe219aeb51.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A cascaded system with four transfer functions. Notice the general transfer function is just the scaler multiplication of each transfer functions.</span></figcaption></figure><p>As you can see from the figure above, we can express a really complex transfer function with high-order components as a cascaded individual system, with simple equations. </p><div class="kg-card kg-toggle-card" data-kg-toggle-state="close">
            <div class="kg-toggle-heading">
                <h4 class="kg-toggle-heading-text"><span style="white-space: pre-wrap;">Linear and Time-Invarient Systems </span></h4>
                <button class="kg-toggle-card-icon" aria-label="Expand toggle to read content">
                    <svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                        <path class="cls-1" d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311"/>
                    </svg>
                </button>
            </div>
            <div class="kg-toggle-content"><p><span style="white-space: pre-wrap;">In this guide, we particularly focus on the linear and time-invariant systems, which means that:</span></p><ul><li value="1"><span style="white-space: pre-wrap;">If you double the input, you&apos;d see a doubled output.</span></li><li value="2"><span style="white-space: pre-wrap;">It doesn&apos;t matter if you calculate the output today or tomorrow.</span></li></ul><p><span style="white-space: pre-wrap;">This acceptance of linear and time-invariant systems is going to make our lives easy. </span></p></div>
        </div><p>For the digital signal case, we try to imagine the most general block-box system and try to find its transfer function. First of all, an output discrete signal $y[n]$ will be dependent on a discrete input $x[n]$.</p><p>$$<br>y[n] = b_0 \cdot x[n]<br>$$ </p><p>It is also possible that $y[n]$ is dependent on its previous input $x[n-1]$, which is called feedforward, or on previous ones.</p><p>$$<br>y[n] = b_0 \cdot x[n] + b_1 \cdot x[n-1] + b_2 \cdot x[n-2] + b_3 \cdot x[n-3] + \text{...}<br>$$</p><p>It is also possible that $y[n]$ is dependent on its previous outputs $y[n-1]$, which is called feedback, or on previous ones.</p><p>$$<br>y[n] = \sum_{\alpha = 0}^{n} b_\alpha \cdot x[n-\alpha] + \sum_{\beta = 0}^{n-1} a_\beta \cdot y[n-\beta]<br>$$</p><p>It&apos;s also possible that $y[n]$ can have a scalar factor. Let&apos;s collect $y[n-\beta]$s to the left and $x[n-\alpha]$s to the right. We don&apos;t need to care about the coefficient&apos;s sign since we describe them for our ease.</p><p>$$<br>\sum_{\beta = 0}^{n} a_\beta \cdot y[n-\beta] = \sum_{\alpha = 0}^{n} b_\alpha \cdot x[n-\alpha]<br>$$</p><p>This is the most general equation we can have for a digital signal system. To find a transfer function as the ratio of outputs to inputs, we need to find a way to express all of the outputs in one output variable and all of the inputs in one input variable. Luckily, this is what we can do with the Z-transform, and its time shifting property!</p><p>Let me remind you of the Z-transform of the basic input and output to show how they can help us to unify everything into $Y(z)$ and $X(z)$.</p><p>$$<br>\mathcal{Z}\{x[n-\alpha]\} = z^{-\alpha}X(z)<br>$$</p><p>$$<br>\mathcal{Z}\{y[n-\beta]\} = z^{-\beta}Y(z)<br>$$</p><p>Let&apos;s apply the Z-transform to the all system equation.</p><p>$$<br>\sum_{\beta = 0}^{n} a_\beta \cdot z^{-\beta}Y(z) = \sum_{\alpha = 0}^{n} b_\alpha \cdot z^{-\alpha}X(z)<br>$$</p><p>Since a transfer function is defined as $H(z) = Y(z) / X(z)$, we can reorganise the equation and find the transfer function of a digital signal system.</p><p>$$<br>H(z) = \frac{Y(z)}{X(z)} = \frac{\sum_{\alpha = 0}^{n} b_\alpha \cdot z^{-\alpha}}{\sum_{\beta = 0}^{n} a_\beta \cdot z^{-\beta}}<br>$$</p><p>The form that most of us see in the textbooks is as follows. It is clearer to express it this way since there&apos;s no more $\alpha$ and $\beta$ magic.</p><p>$$<br>H(z) = \frac{Y(z)}{X(z)} = \frac{b_0 \cdot z^{-0} + b_1 \cdot z^{-1} + b_2 \cdot z^{-2} + b_3 \cdot z^{-3} + \text{...}}{a_0 \cdot z^{-0} + a_1 \cdot z^{-1} + a_2 \cdot z^{-2} + a_3 \cdot z^{-3} + \text{...}}<br>$$</p><p>We derived the most important thing we&apos;ll use to design our filters. We&apos;ll use this equation to define a filter, and talk about what it tells us.</p><h3 id="basics-of-digital-filters">Basics of Digital Filters</h3><p>Many define filters based on different aspects. I like to define them as any transfer function that is applied to a digital signal. This is why in the current section, we never talked about a filter. In the end, they are not special; just a simple black-box system where we know what we want to get as an output based on an input pair. The general transfer function we found in the previous section is a transfer function of a digital filter as well.</p><p>The coefficients of $Y(z)$ are known as $a$ coefficients, and $X(z)$ are b coefficients. One can design a filter based on only the coefficients since it can represent the system response as a combination of feedforward and feedback loops.</p><p><strong>Feedback</strong> is to change the current output based on the previous output. In time-domain representation, it means that $y[n]$ is affected by $y[n-1]$. We represented it with $a$-coefficients. The roots of a feedback are called <strong>poles</strong>. Because the filter feeds its own previous calculations back into itself to create the new one, its response to an impulse can mathematically ring out forever, creating<br>an <strong>Infinite Impulse Response (IIR)</strong>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-c35c0969-f725-4425-91ed-8ddd35f2d951.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1189" height="616" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-c35c0969-f725-4425-91ed-8ddd35f2d951.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-c35c0969-f725-4425-91ed-8ddd35f2d951.png 1000w, https://gyokhan.com/content/images/2026/04/data-src-image-c35c0969-f725-4425-91ed-8ddd35f2d951.png 1189w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A simple IIR filter showcase. We give an input of an impulse (x[n] = 1 for only n = 0) to the system, and it produces an output that never reaches zero.</span></figcaption></figure><p><strong>Feedforward</strong> is to change the current output based on previous inputs. In time-domain representation, it means that $y[n]$ is affected by $x[n]$, $x[n-1]$, etc. We represented it with $b$-coefficients. The roots of a feedforward are called <strong>zeros</strong>. If a filter only uses $b$ coefficients, it has a <strong>Finite Impulse Response (FIR)</strong> because the signal eventually flushes out of the system.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-4b62a3e6-f4bb-4e6a-be19-d0fe43fbc3c2.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="1189" height="616" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-4b62a3e6-f4bb-4e6a-be19-d0fe43fbc3c2.png 600w, https://gyokhan.com/content/images/size/w1000/2026/04/data-src-image-4b62a3e6-f4bb-4e6a-be19-d0fe43fbc3c2.png 1000w, https://gyokhan.com/content/images/2026/04/data-src-image-4b62a3e6-f4bb-4e6a-be19-d0fe43fbc3c2.png 1189w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A simple FIR filter showcase. We give an input of an impulse (x[n] = 1 for only n = 0) to the system, and it produces an output that eventually flattens.</span></figcaption></figure><h4 id="poles-and-zeros">Poles and Zeros</h4><p>The poles and zeros are quite important since they define the nature of our filter and its output behaviour. Zeros are the roots of the numerator, and poles are the roots of the denominator.</p><p>Before trying to understand what zeros and poles do to our input and output mapping, let me remind you what information the $z$ variable holds. It is an alias defined as $z = re^{j\omega}$ as we discussed earlier. The $r$ variable holds the dumping factor, which tells us if our signal will be zeroed out, grow, or stay as is. $\omega$ is the angular frequency in radians per second, which is defined as $\omega = 2 \pi f / f_{sr}$ where $f$ is the frequency, and $f_{sr}$ is the sampling rate. Therefore $\omega$ variable holds the frequency component of the response. We are going to use this knowledge to understand how zeros and poles affect our output.</p><p>We know that the numerator of any ratio can be zero, which results in the output being 0 as well. This means that the frequencies in a filter that make the numerator zero, the filter will zero out/cancel them. It is not important what dumping factor we have; the frequency will be zero-out.</p><p>On the other hand, mathematically, it is not possible to have a ratio divided by zero. We can only approach zero in the denominator using a limit, which results in our ratio&apos;s result going to infinity. This is true in the case of a filter&apos;s poles as well. The frequencies that make our denominator zero might cause the filter to have an unstable response, depending on the dumping factor. For a dumping factor $r &gt; 1$, the filter will generate a response that grows indefinitely, and cause the system to be burned and smell bad. If the dumping factor is exactly 1, $r = 1$, we can still conclude that the system will be unstable since the previous consistent output will be added to the next output in a feedback loop, and it will generate an unstable system where the output grows to infinity. Again, the same bad smell. On the other hand, for any dumping factor $r &lt; 1$, the system is considered stable since the response will be zero out eventually.</p><p>So, in a summary,</p><ul><li><strong>Zeros</strong> are the roots that reduce the signal of a frequency and eventually zero outs them.</li><li><strong>Poles</strong> are the roots that increase the signal of a frequency and eventually goes infinity if $r \geq 1$.</li></ul><p>One can calculate zeros and poles for any filter design to understand its stability by solving for the variable $z = r e^{j\omega} = r e^{j 2 \pi f / f_{sr}}$ for the equations below.</p><p>$$<br>\text{Zeros} \rightarrow b_0 \cdot z^{-0} + b_1 \cdot z^{-1} + b_2 \cdot z^{-2} + b_3 \cdot z^{-3} + \text{...} = 0<br>$$</p><p>$$<br>\text{Poles} \rightarrow a_0 \cdot z^{-0} + a_1 \cdot z^{-1} + a_2 \cdot z^{-2} + a_3 \cdot z^{-3} + \text{...} = 0<br>$$</p><p>It is important to note that we want our systems to be causal, which means that our system should be only dependent on the previous values and not future ones. When a transfer function where the degree of the numerator was higher than the denominator, without a corresponding delay, it would be trying to calculate an output using a future input. Therefore, we need to make sure that we have the same number of poles and zeros (roots of the numerator and denominator).</p><p>It can be easily realised by placing poles at $z = 0$, which won&apos;t have any effect on the system&apos;s amplitude response but cause a time delay, which is what we want to not be dependent on future values. On the other hand, we do not need to place zeros at infinity explicitly to match the number of poles since the nature of the equation introduces them implicitly. </p><h4 id="unit-circle">Unit Circle</h4><p>We can use a complex plane where the vertical axis shows the imaginary part and the horizontal axis shows the real part of the $z$ variable. The poles and zeros can be placed into this plane to visualise their stability easily. Furthermore, we can have a unit circle on $|z| = 1$ to indicate the dumping factor effect of IIR filters. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-96e731dd-8be4-4470-affc-0b33d4ae970d.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="478" height="470"><figcaption><span style="white-space: pre-wrap;">An unit circle (r =1) in a complex plane where vertical axis is imaginary component of z, and horizontal axis is real component of z.</span></figcaption></figure><p>Within this plane, we can place our zeros and poles within a distance to the origin of $r$ (in the $z$). First of all, the signal&apos;s frequency is encoded on the $Re\{z\}$ axis, where 0 degrees (or 0 radians) represents the lowest frequency at $(1, 0)$, and 180 degrees (or $\pi$ radians) represents the highest frequency at $(-1, 0)$.</p><p>One can calculate it using $z = r e^{j 2 \pi f / f_{sr}}$ and inserting the values into the equation for the highest frequency (based on the Nyquist rate). I&apos;ll set $r = 1$ to only consider one type of dumping, the steady-state one, but this can be applied to all other $r$s. </p><p>$$<br>z = r \cdot e^{j 2 \pi f / f_{sr}} = 1 \cdot e^{j 2 \pi 22050 / 44100} = e^{j \pi} = -1<br>$$</p><p>The maximum frequency resides on $Re{z} = -1$ and $Im{z} = 0$. And with the lowest frequency, 0 Hz,</p><p>$$<br>z = r \cdot e^{j 2 \pi f / f_{sr}} = 1 \cdot e^{j 2 \pi 0 / 44100} = e^{0} = 1<br>$$</p><p>The minimum frequency resides on $Re{z} = 1$ and $Im{z} = 0$. Let&apos;s do it for half of the maximum frequency as well, to understand how it flows from max to min,</p><p>$$<br>z = r \cdot e^{j 2 \pi f / f_{sr}} = 1 \cdot e^{j 2 \pi 11025 / 44100} = e^{j 0.5} = +j<br>$$</p><p>The half of the maximum frequency resides on $Re{z} = 0$ and $Im{z} = -1$. That&apos;s perfect. It shows that the frequencies for the $r = 1$ case are like walking on the top half of a circle. For any other $r$, imagine the same walk, but with a circle of a different radius.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-cfd156be-a68f-490f-ac39-86ffcba0809b.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="644" height="624" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-cfd156be-a68f-490f-ac39-86ffcba0809b.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-cfd156be-a68f-490f-ac39-86ffcba0809b.png 644w"><figcaption><span style="white-space: pre-wrap;">An unit circle (r = 1) shown with dashed lines and the figure shows the walk from the lowest frequency to the highest.</span></figcaption></figure><p>You may ask why we have the negative side of the imaginary plane. We will use that part to present complex conjugates of the roots on the top side, resulting in real numbers for $a$ and $b$ coefficients.</p><p>When designing a filter, the unit circle or the complex Z-plane is used to show the poles (with X) and zeros (with O) to understand the stability visually.</p><h3 id="filter-design">Filter Design</h3><ul><li>Frequencies to Poles and Zeros Calculation</li><li>Poles and Zeros to A and B coefficients</li><li>Transfer function with A and B to Bode Plots</li></ul><p>The design of a digital filter starts with gathering the requirements of the filter, such as what frequencies should be suppressed, what frequencies should be resonated (grow), and how steep we want the transitions to be, etc.</p><p>When all the requirements are set, we convert all the frequencies to the poles and zeros and plot them on the complex Z-plane (with unit circle) to see the stability of the system, and modify them if necessary. After the placement of poles and zeros is ready, the filter&apos;s frequency response is drawn with the x-axis as frequency on a logarithmic scale and the y-axis as amplitude in decibels. The same plot is drawn for the phase in the y-axis to understand the delay of the system &#x2013; this is out of the scope for now. This plot is called a Bode plot and is quite useful to understand how our filter behaves.</p><div class="kg-card kg-toggle-card" data-kg-toggle-state="close">
            <div class="kg-toggle-heading">
                <h4 class="kg-toggle-heading-text"><b><strong style="white-space: pre-wrap;">Respect to Butterworth</strong></b><span style="white-space: pre-wrap;">, </span><b><strong style="white-space: pre-wrap;">Chebyshev and Others</strong></b></h4>
                <button class="kg-toggle-card-icon" aria-label="Expand toggle to read content">
                    <svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                        <path class="cls-1" d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311"/>
                    </svg>
                </button>
            </div>
            <div class="kg-toggle-content"><p><span style="white-space: pre-wrap;">Although what I describe in this section is the manual process of designing filters, we as humanity are not in this simple phase anymore. The design of a filter with many poles and zeros is quite challenging in its mathematics. Furthermore, there are not that well-defined rulesets of how steep it would be -- dB reduction per octave. Therefore, in the modern world, we tend to follow the best practices in filter design with blueprint designs. The most important ones are from electrical engineers&apos; old calculations: Butterworth Filter, Chebyshev Filter, Bessel Filter and Cauar Filter. It is easier to use their topology and change the parameters to fulfil our needs of steepness with orders and use pre-calculated tables for high dimensions.</span></p></div>
        </div><p>I want to make some examples in this section to show you how we can design a filter from scratch &#x2013; and as fast as possible. The filters will be low-pass, high-pass filters, a low-pass resonator, a notch filter, and a bandpass.</p><h4 id="low-pass-filter">Low-Pass Filter</h4><p>As the name suggests, it is a filter that only allows low frequencies to pass and blocks high frequencies. However, when a signal passes through a filter, the transition from &quot;pass&quot; to &quot;block&quot; isn&apos;t a sudden change. It is a gradual slope. Therefore, as engineers, we had to define a threshold that would indicate when the &quot;cutoff&quot; actually happens. Therefore, by the standards, we call this cutoff to happen on the exact point where the signal loses exactly half of its original energy/power. By the definition of decibels, we can calculate this point as $-3.01$ dB.</p><p>$$<br>10 \cdot \log_{10}(\frac{\text{Current Power}}{\text{Total Power}}) = 10 \cdot \log_{10}(\frac{\alpha}{2\alpha}) = 10 \cdot \log_{10}(0.5) = -3.01 \text{dB}<br>$$</p><p>Let&apos;s design the simplest low-pass filter first by adding a zero to the highest frequency, which is $z = -1$ in the unit circle. Please remember that we have to introduce a pole to have the same number of poles and zeros to make the system causal.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-b4256b96-5a05-46b1-a762-e735f65e379e.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="478" height="470"><figcaption><span style="white-space: pre-wrap;">A unit-circle diagram shows poles and zeros for a simple low-pass filter.</span></figcaption></figure><p>So, we have a zero at $z = -1$ and a pole at $z = 0$. It means that our transfer function has a first-order polynomial in its numerator and denominator.</p><p>$$<br>H(z) = \frac{z - z_0}{z - z_x} = \frac{z + 1}{z - 0}<br>$$</p><p>We want to express the transfer function in the form of delay shifts of $z^{-k}$, therefore, we are going to divide the terms by $z$ to find our $a$ and $b$ coefficients.</p><p>$$<br>H(z) =  \frac{z + 1}{z} = \frac{1 + z^{-1}}{1 + 0 \cdot z^{-1}}<br>$$</p><p>We can see that our $a$ coefficients are $[1, 0]$ and $b$ coefficients are $[1, 1]$. This transition from zero-pole to transfer functions&apos; $a$ and $b$ coefficients are easy for a few poles, but hard for many. Therefore, from now on, I&apos;ll use <code>scipy</code>&apos;s <code>signal.zpk2tf</code> function. It gets poles, zeros and a magnitude factor $k$ and outputs two arrays that are our transfer function&apos;s coefficients.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from scipy import signal

# Define your zeros and poles.
zeros = [-1.0]
poles = [0.0]

# Convert Zeros, Poles, and a Gain (k=1.0) into b and a arrays.
# We don&apos;t care about gain in this guide. Set it to 1.0.
b, a = signal.zpk2tf(zeros, poles, k=1.0)

# SciPy will sometimes return very tiny imaginary rounding errors.
# We can safely cast it to purely real numbers.
b = np.real(b)
a = np.real(a)</code></pre><figcaption><p><span style="white-space: pre-wrap;">A sample Python code block that converts any zeros and poles to a and b coefficents of transfer function.</span></p></figcaption></figure><p>It&apos;s time to make a Bode plot to see the response of the filter for all frequencies from 0 Hz to the Nyquist frequency. To calculate the filter&apos;s response, one can use <code>scipy</code>&apos;s <a href="https://docs.scipy.org/doc/scipy-1.16.0/reference/generated/scipy.signal.freqz.html" rel="noreferrer"><code>signal.freqz</code></a> function. It gets the transfer function as input and calculates the response of each frequency.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from scipy import signal

# The output w is the frequencies.
# The output h is the response.
w, h = signal.freqz(b, a)
</code></pre><figcaption><p><span style="white-space: pre-wrap;">A simple Python code to calculate the frequency response of a transfer funciton.</span></p></figcaption></figure><p>I&apos;m not going into detail on how to plot a Bode plot, but you can find the code that I use in Appendix A1. You can see the magnitude and the phase frequency response in the next figure.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-d84b94eb-7b30-4a62-be40-05b90edc98ce.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="989" height="790" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-d84b94eb-7b30-4a62-be40-05b90edc98ce.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-d84b94eb-7b30-4a62-be40-05b90edc98ce.png 989w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The Bode plot of the low-pass filter.</span></figcaption></figure><p>The current cutoff frequency is around 11 kHz. The frequencies up to the cutoff are allowed to pass, but those after are not. The cutoff being nearly the middle of the frequency spectrum is not a coincidence. We chose to place the zero at the maximum frequency and haven&apos;t placed any poles or zeros on the frequencies from 0 Hz to 11 kHz. We can move the cutoff frequency by placing more zeros and poles.</p><h4 id="high-pass-filter">High-Pass Filter</h4><p>The same logic applies to the high-pass filter design as well. We want to have our zero on lowest frequency (which is $z = 1$). We need to place a pole at $z = 0$ to make sure our system is causal. Instead of calculating from scratch, I&apos;ll use Python to calculate $a$ and $b$ coefficients from zeros and poles and plot the Bode plot.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from scipy import signal

zeros = [1.0]
poles = [0.0]

# Find the transfer function coefficients.
b, a = signal.zpk2tf(zeros, poles, k=1.0)
b = np.real(b)  # b: [1.0, -1.0]
a = np.real(a)  # a: [1.0, 0.0]

# Calculate the frequency response. 
w, h = signal.freqz(b, a)</code></pre><figcaption><p><span style="white-space: pre-wrap;">The Python code to calculate the frequency response of a high-pass filter with a zero on z = 1.0.</span></p></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-3a994a54-cbf5-4f6e-9ff3-4cc8ef3ce113.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="989" height="790" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-3a994a54-cbf5-4f6e-9ff3-4cc8ef3ce113.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-3a994a54-cbf5-4f6e-9ff3-4cc8ef3ce113.png 989w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The Bode plot of the high-pass filter.</span></figcaption></figure><p>You can see that the lowest frequencies start from -40dB, and we can see half of the maximum power on 11kHz again.</p><h4 id="amplifier">Amplifier</h4><p>An amplifier is a signal grower in a particular frequency range. It is the opposite of a high/low-pass filter. A high-pass amplifier is going to resonate the highest frequencies. It is being made by adding a pole to the highest frequency ($z = -1$). Please remember that if we place a pole in the unit circle and beyond, we&apos;ll have an unstable filter. Therefore, in an amplifier, a pole needs to be close to the unit circle but not on it. I&apos;m choosing $z = -0.99$ for the pole.</p><figure class="kg-card kg-code-card"><pre><code>from scipy import signal

zeros = [0.0]
poles = [-0.99]

# Find the transfer function coefficients.
b, a = signal.zpk2tf(zeros, poles, k=1.0)
b = np.real(b)  # b: [1.0, 0.0]
a = np.real(a)  # a: [1.0, 0.99]

# Calculate the frequency response. 
w, h = signal.freqz(b, a)</code></pre><figcaption><p><span style="white-space: pre-wrap;">The Python code to calculate the frequency response of a high-pass amplifier with a pole on z = -0.99.</span></p></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-c2eef651-9baa-43ce-9b4f-3a9b57fd17d1.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="989" height="789" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-c2eef651-9baa-43ce-9b4f-3a9b57fd17d1.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-c2eef651-9baa-43ce-9b4f-3a9b57fd17d1.png 989w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The Bode plot of the high-pass amplifier.</span></figcaption></figure><p>A low-pass amplifier is a filter that amplifies the magnitude of low frequencies. This time, we should set our pole close to $z \approx 1$.</p><figure class="kg-card kg-code-card"><pre><code>from scipy import signal

zeros = [0.0]
poles = [0.99]

# Find the transfer function coefficients.
b, a = signal.zpk2tf(zeros, poles, k=1.0)
b = np.real(b)  # b: [1.0, 0.0]
a = np.real(a)  # a: [1.0, -0.99]

# Calculate the frequency response. 
w, h = signal.freqz(b, a)</code></pre><figcaption><p><span style="white-space: pre-wrap;">The Python code to calculate the frequency response of a low-pass amplifier with a pole on z = 0.99.</span></p></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-c007ac53-3cfd-427c-9b3c-1ed0f7ab7010.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="989" height="790" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-c007ac53-3cfd-427c-9b3c-1ed0f7ab7010.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-c007ac53-3cfd-427c-9b3c-1ed0f7ab7010.png 989w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The Bode plot of the low-pass amplifier.</span></figcaption></figure><h4 id="notch-filter">Notch Filter</h4><p>Some applications require us to have a notch in the magnitude plot of the frequency spectrum. It is particularly useful to remove particular frequencies from our output, like a noise remover. It is actually a zero in a unit circle for a frequency. Imagine it as if we&apos;re placing a zero not on the edges of the spectrum like we did for high-pass or low-pass, but for a specific frequency.</p><p>Let&apos;s design a system that will reduce the response to 4.4 kHz. First of all, we need to find the zero from the frequency. I&apos;ll assume the sampling rate as 44.1 kHz.</p><p>$$<br>\omega = 2 \cdot \pi \frac{4400}{44100} \approx \frac{\pi}{5}<br>$$ </p><p>We can find the z value by substituting the $\omega$ and using $r = 1$.</p><p>$$<br>z = r \cdot e^{j \omega} = 1 \cdot e^{j \frac{\pi}{5}} = e^{j \frac{\pi}{5}}<br>$$</p><p>As one can understand from the z value, it is a complex variable. However, in real life, it is not possible to have complex coefficients in our transfer function. Therefore, we need to add the complex conjugate pair of the calculated zero to express the transfer function in real coefficients.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from scipy import signal

# Set zeros and poles. Make sure to have complex conjugates in zeros
# to resolve to real values.
zeros = [
    1.0 * np.exp(1j * np.pi / 5),
    1.0 * np.exp(-1j * np.pi / 5),
]
poles = [0.0, 0.0]

# Calculate the transfer function.
b, a = signal.zpk2tf(zeros, poles, k=1.0)
b = np.real(b) # b: [1.0, -1.618, 1.0]
a = np.real(a) # a: [1.0, 0.0, 0.0]

show_filter_response(b, a)</code></pre><figcaption><p><span style="white-space: pre-wrap;">The Python code to calculate the frequency response of a notch filter that supresses 4.4kHz.</span></p></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-7a944f2a-7a0e-410b-9f8a-0d9a315ee366.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="989" height="790" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-7a944f2a-7a0e-410b-9f8a-0d9a315ee366.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-7a944f2a-7a0e-410b-9f8a-0d9a315ee366.png 989w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The Bode plot of the notch filter at 4.4kHz.</span></figcaption></figure><p>We can apply the same logic by creating a pole instead of a zero to present a notch amplifier.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from scipy import signal

# Set zeros and poles. Make sure to have complex conjugates in poles
# to resolve to real values.
zeros = [0.0, 0.0]
poles = [
    1.0 * np.exp(1j * np.pi / 5),
    1.0 * np.exp(-1j * np.pi / 5),
]

# Calculate the transfer function.
b, a = signal.zpk2tf(zeros, poles, k=1.0)
b = np.real(b) # b: [1.0, 0.0, 0.0]
a = np.real(a) # a: [1.0, -1.618, 1.0]

show_filter_response(b, a)</code></pre><figcaption><p><span style="white-space: pre-wrap;">The Python code to calculate the frequency response of a notch amplifier that amplifies 4.4kHz.</span></p></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-c8246030-3a2a-445e-8368-75578298c4cf.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="989" height="790" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-c8246030-3a2a-445e-8368-75578298c4cf.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-c8246030-3a2a-445e-8368-75578298c4cf.png 989w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The Bode plot of the notch amplifier at 4.4kHz.</span></figcaption></figure><p>I presented the cutoff frequency in the figure above. Although it shows the -3dB point, the purpose of drawing it is to show you on what frequency we achieved the notch.</p><h4 id="bandpass-filter">Bandpass Filter</h4><p>A bandpass filter allows the system to pass only a frequency range between two cutoff frequencies. Based on our previous examples, it is a cascaded system that has a high-pass filter with cutoff frequency $f_{c1}$ and a low-pass filter with cutoff frequency $f_{c2}$ where $f_{c2} &gt; f_{c1}$.</p><p>By experimenting with the most basic bandpass filter, we can set the zeros on both ends of the frequency spectrum: a zero on $z=1$ for low frequencies and a zero on $z=-1$ for high frequencies.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from scipy import signal

# Set zeros and poles. Make sure to have complex conjugates in poles
# to resolve to real values.
zeros = [1.0, -1.0]
poles = [0.0, 0.0]

# Calculate the transfer function.
b, a = signal.zpk2tf(zeros, poles, k=1.0)
b = np.real(b) # b: [1.0, 0.0, -1.0]
a = np.real(a) # a: [1.0, 0.0, 0.0]

show_filter_response(b, a)</code></pre><figcaption><p><span style="white-space: pre-wrap;">The Python code to calculate the frequency response of a bandpass filter.</span></p></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-1a8bfb7a-b328-46f6-b21c-026e62ca4b95.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="987" height="790" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-1a8bfb7a-b328-46f6-b21c-026e62ca4b95.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-1a8bfb7a-b328-46f6-b21c-026e62ca4b95.png 987w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The Bode plot of the bandpass filter at z = 1 and z = -1. </span></figcaption></figure><p>As you can see, the frequency spectrum is divided into two splits. The first low-frequency split ranges from 0 Hz to 11 kHz. Our zero on $z = 1$ placed the cutoff to its middle frequency, $f_{c1} \approx 5500$ Hz. The second half frequency split ranges from 11 kHz to 22 kHz. The other zero placed the cutoff to its middle frequency, again, $f_{c1} \approx 16500$ Hz.</p><p>However, the shape of the bandpass filter is not as straight as we expect it to be from the textbook figures. The reason behind it is the lack of poles that will amplify the cutoffs to make the top end like a straight line. Let&apos;s make another bandpass filter that uses poles to define a better range of frequencies to pass, and a better uniform magnitude shape on passing frequencies. </p><p>The objective is to design a bandpass filter that allows signals between 2.2 kHz and 4.4 kHz to pass. First of all, we&apos;ll have a low-pass filter and a high-pass filter with $z=1$ and  $z=-1$. This will reduce the magnitudes of signals on both ends of the frequency spectrum. Then, we need to have notch amplifiers on the frequencies we want them to pass.</p><p>For frequency 4.4 kHz, we already calculated the z value as $z_1 = e^{j \frac{\pi}{5}}$. We&apos;re also going to have its complex conjugate to result in a real-valued transfer function, $z_2 = e^{-j \frac{\pi}{5}}$. For frequency 2.2 kHz, the z value can be calculated as the following equation.  We will have the complex conjugate of the $z_3$ as $z_4$.</p><p>$$<br>z_3 = r \cdot e^{j\omega} = r \cdot e^{j \cdot 2 \pi \frac{f}{f_{sr}}} = 0.90 \cdot e^{j \cdot 2 \ pi 2200 / 44100} = e^{j \ pi / 10}<br>$$</p><p>Please notice that I selected $r = 0.90$. It is based on my experiences with how high peaks I want to have. With a value of 0.99, the peaks were so high that a straight shape wasn&apos;t possible. I see that 0.85 to 0.90 values are feasible.</p><p>Therefore, in the end, we can use the following Python script to calculate the $a$ and $b$ coefficients and plot the frequency response.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from scipy import signal

# Set zeros and poles. Make sure to have complex conjugates in poles
# to resolve to real values.
zeros = [1.0, -1.0]
poles = [
    0.90 * np.exp(1j * np.pi / 10),
    0.90 * np.exp(-1j * np.pi / 10),
    0.90 * np.exp(1j * np.pi / 5),
    0.90 * np.exp(-1j * np.pi / 5)
]

b, a = signal.zpk2tf(zeros, poles, k=1.0)
b = np.real(b)
a = np.real(a)

show_filter_response(b, a)</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">The Python code to calculate the frequency response of a bandpass filter.</span></p></figcaption></figure><figure class="kg-card kg-image-card"><img src="https://gyokhan.com/content/images/2026/04/data-src-image-64b3dee6-f429-4662-8a42-9282bc8db6e7.png" class="kg-image" alt="Digital Filter Design in 30 minutes" loading="lazy" width="989" height="790" srcset="https://gyokhan.com/content/images/size/w600/2026/04/data-src-image-64b3dee6-f429-4662-8a42-9282bc8db6e7.png 600w, https://gyokhan.com/content/images/2026/04/data-src-image-64b3dee6-f429-4662-8a42-9282bc8db6e7.png 989w" sizes="(min-width: 720px) 720px"></figure><p>In the figure above, you can see that we have nearly a perfect bandpass filter whose cutoffs are defined as $f_{c1} = 1.9$ kHz and $f_{c2} = 4.8$ kHz. </p><h3 id="last-words-and-discussions">Last Words and Discussions</h3><p>Up to this section, I tried to explain how digital signal processing works and what the procedure is to design simple digital filters manually. There are better methods, of course, or better teachers, but I believe this guide will be a great getting-started guide for the people who are afraid of starting to learn this magnificent topic.</p><p>Thanks again to everyone for reading it up to now. In the era of artificial intelligence, it was a great opportunity for me to write what I learned without any help from LLM models. I&apos;m going to suggest that everyone follow the path of DSP by understanding how to go from cutoff frequencies to filters instead of the other way around. There&apos;s a method called Bilinear Transformation, which helps you to do it. Then, the next stop should be popular filters like Butterworth and Chebyshev.</p><p>I&apos;m trying to develop a DSP library in Mojo language, as I said earlier, you can also check the library out to see how one can implement these algorithms we discussed in the computational domain.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/electricalgorithm/dsplib-mojo"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - electricalgorithm/dsplib-mojo: An introduction to digital signal processing with Mojo language.</div><div class="kg-bookmark-description">An introduction to digital signal processing with Mojo language. - electricalgorithm/dsplib-mojo</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt="Digital Filter Design in 30 minutes"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">electricalgorithm</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/194d437938a9792b96613c2c4d2996eed573cae3084b45a0e572400edf330c8d/electricalgorithm/dsplib-mojo" alt="Digital Filter Design in 30 minutes"></div></a></figure><p>Have fun!</p><p>Feel free to contact me for any typos, mistakes or questions: hello@gyokhan.com  </p><hr><h3 id="appendix">Appendix</h3><h4 id="appendix-a1-interactive-bode-plot-in-python">Appendix A1. Interactive Bode Plot in Python</h4><p>I&apos;m using the following Python code block to generate the graphs to make them interactive.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

import numpy as np
import matplotlib.pyplot as plt

import plotly.graph_objects as go
from plotly.subplots import make_subplots


def plot_bode_interactive(w, h, fs=44100, y_axis_limits=[-60, 10]):
    freq_hz = (w * fs) / (2 * np.pi)
    # Using a floor of 1e-12 to prevent math errors, though the plot range will hide this
    magnitude_db = 20 * np.log10(np.abs(h) + 1e-12)
    phase_deg = np.angle(h, deg=True)

    fig = make_subplots(rows=2, cols=1,
                        shared_xaxes=True,
                        vertical_spacing=0.1,
                        subplot_titles=(&quot;Magnitude Response&quot;, &quot;Phase Response&quot;))

    # Magnitude Trace
    fig.add_trace(
        go.Scatter(x=freq_hz, y=magnitude_db, name=&quot;Magnitude&quot;,
                   line=dict(color=&apos;#1f77b4&apos;, width=2),
                   hovertemplate=&apos;&lt;b&gt;Freq&lt;/b&gt;: %{x:.2f} Hz&lt;br&gt;&lt;b&gt;Mag&lt;/b&gt;: %{y:.2f} dB&lt;extra&gt;&lt;/extra&gt;&apos;),
        row=1, col=1
    )

    # Fixed -3dB line to show half power point.
    fig.add_hline(y=-3, line_dash=&quot;dot&quot;, line_color=&quot;red&quot;, opacity=0.5, row=1, col=1)

    # Phase Trace
    fig.add_trace(
        go.Scatter(x=freq_hz, y=phase_deg, name=&quot;Phase&quot;,
                   line=dict(color=&apos;#2ca02c&apos;, width=2),
                   hovertemplate=&apos;&lt;b&gt;Freq&lt;/b&gt;: %{x:.2f} Hz&lt;br&gt;&lt;b&gt;Phase&lt;/b&gt;: %{y:.2f}&#xB0;&lt;extra&gt;&lt;/extra&gt;&apos;),
        row=2, col=1
    )

    # Limit X-axis to standard range (20Hz to Nyquist)
    fig.update_xaxes(type=&quot;log&quot;, range=[np.log10(20), np.log10(fs/2)],
                     title_text=&quot;Frequency (Hz)&quot;, gridcolor=&apos;lightgray&apos;)

    # Limit Y-axis so we don&apos;t see the floors
    # This keeps the focus on the passband and the transition
    fig.update_yaxes(range=y_axis_limits, title_text=&quot;Magnitude (dB)&quot;, row=1, col=1, gridcolor=&apos;lightgray&apos;)

    fig.update_yaxes(title_text=&quot;Phase (degrees)&quot;, row=2, col=1, gridcolor=&apos;lightgray&apos;)

    fig.update_layout(
        height=800,
        template=&quot;plotly_white&quot;,
        showlegend=False,
        hovermode=&quot;x unified&quot;
    )
    fig.show()</code></pre><figcaption><p><span style="white-space: pre-wrap;">A Python function that visualizes Bode plot interactively with -3dB line for cut-off analysis.</span></p></figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Why I stopped using JSON for MQTT and use Zig to develop gRPC-like communication?]]></title><description><![CDATA[<p>I&apos;ve spent the last few weeks building <a href="https://github.com/electricalgorithm/protomq" rel="noreferrer">ProtoMQ</a>, an MQTT broker that validates and encodes messages using Protocol Buffers &#x2014; with both the MQTT parser and the Protobuf engine written from scratch in Zig.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/electricalgorithm/protomq"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - electricalgorithm/protomq: ProtoMQ: Type-safe, bandwidth-efficient MQTT for the rest of us. Stop</div></div></a></figure>]]></description><link>https://gyokhan.com/stopped-using-json-for-mqtt-and-develop-zig-protobuf-mqtt/</link><guid isPermaLink="false">69a21ef14da3760001dbabed</guid><category><![CDATA[embedded systems]]></category><category><![CDATA[zig]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Fri, 27 Feb 2026 23:30:00 GMT</pubDate><media:content url="https://gyokhan.com/content/images/2026/02/terminal_demo.svg" medium="image"/><content:encoded><![CDATA[<img src="https://gyokhan.com/content/images/2026/02/terminal_demo.svg" alt="Why I stopped using JSON for MQTT and use Zig to develop gRPC-like communication?"><p>I&apos;ve spent the last few weeks building <a href="https://github.com/electricalgorithm/protomq" rel="noreferrer">ProtoMQ</a>, an MQTT broker that validates and encodes messages using Protocol Buffers &#x2014; with both the MQTT parser and the Protobuf engine written from scratch in Zig.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/electricalgorithm/protomq"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - electricalgorithm/protomq: ProtoMQ: Type-safe, bandwidth-efficient MQTT for the rest of us. Stop sending bloated JSON over the wire.</div><div class="kg-bookmark-description">ProtoMQ: Type-safe, bandwidth-efficient MQTT for the rest of us. Stop sending bloated JSON over the wire. - electricalgorithm/protomq</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt="Why I stopped using JSON for MQTT and use Zig to develop gRPC-like communication?"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">electricalgorithm</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/29c502ba38247a05b52fdf6237d41d1b791458d295ec380158953f3819639601/electricalgorithm/protomq" alt="Why I stopped using JSON for MQTT and use Zig to develop gRPC-like communication?"></div></a></figure><hr><h2 id="whats-the-problem">What&apos;s the problem?</h2><p>I love working with IoT devices. In most setups I&apos;ve seen, sensor data flows through MQTT as JSON &#x2014; it&apos;s human-readable, every language has a parser, and you can inspect payloads with mosquitto_sub when something goes wrong. If one&apos;s use-case is not super specific, this would work fine. </p><p>It&apos;s clear that JSON has real costs at the edge:</p><ul>
<li>A 12-field sensor reading takes <strong>310 bytes</strong> in JSON. The same data in Protobuf: <strong>82 bytes</strong>. That&apos;s a 74% reduction. Implementations based on your custom binary format may even reduce it further.</li>
<li>On a cellular-connected device sending data every 5 seconds, that difference adds up to <strong>~1.6 MB/day per device</strong>. Multiply by a fleet of 10,000 devices and you&apos;re looking at 16 GB/day of unnecessary data transfer.</li>
<li>More bytes on the wire means more radio time, which means shorter battery life.</li>
</ul>
<p>Protobuf solves the size problem, but introduces its own drawbacks: you need code generation in every client language, you need to keep generated stubs in sync when schemas change, and debugging becomes harder because payloads are opaque binary.</p><h3 id="why-not-go-with-a-custom-binary-packet-format">Why not go with a custom binary packet format?</h3><p>This is the first thing most embedded teams try (I&apos;ve done it too). You define a packed struct, serialize it on the device, deserialize it on the server. It&apos;s compact &#x2014; often even smaller than Protobuf &#x2014; and dead simple to implement.</p>
<p>The problem shows up about three months in, when you need to add a field.</p>
<p>With a custom binary format, the server and every device in the field must agree on the exact byte layout. There&apos;s no field tags, no length prefixes, no way to tell what version of the struct you&apos;re looking at. Adding a field means every device needs an OTA update <em>before</em> the server can start accepting the new format &#x2014; or you end up maintaining parallel parsers for v1, v2, v3, each with their own offsets and sizes.</p>
<p>This has a few consequences:</p>
<ul>
<li><strong>OTA become critical.</strong> Every schema change requires a firmware update pushed to the entire fleet. If even one device misses the update, its data is silently corrupted or rejected.</li>
<li><strong>End-to-end ownership is mandatory.</strong> You need one team (or one person) who controls the struct definition, the firmware serializer, the server deserializer, and the OTA pipeline. In practice this means the device team and the backend team are tightly coupled on every field change.</li>
<li><strong>No schema introspection.</strong> A new developer joining the project can&apos;t look at the data on the wire and understand it. There&apos;s no <code>.proto</code> file to read, no schema registry to query &#x2014; just a header file somewhere in the firmware repo that may or may not match what&apos;s actually deployed.</li>
</ul>
<p>Protobuf avoids all of this because fields are tagged and the format is self-describing enough to be forward- and backward-compatible. You can add fields without breaking old producers or consumers. ProtoMQ takes it a step further by making the broker the single owner of the schema, so there&apos;s one place to look and one place to update. But that&apos;s for the next sections!</p>
<h2 id="solution-proposal">Solution Proposal</h2><p>So the goal became: get payloads as compact as a custom binary format, but keep clients and the server completely decoupled.</p>
<p>The design I landed is that the broker owns the <code>.proto</code> schemas and maps them to MQTT topics. Clients don&apos;t need to ship with hardcoded struct definitions or pre-compiled stubs. Instead, they discover what schemas exist by querying the broker&apos;s service discovery topic (<code>$SYS/discovery/request</code>), fetch the <code>.proto</code> definition at runtime, and construct their payloads based on that which is a methodology I learned when I was working with gRPC and service discovery. If a schema changes, clients pick up the new version on next discovery &#x2014; no OTA, no redeployment, no version negotiation.</p>
<p>Everything on the wire is binary Protobuf -- because it&apos;s compact enough, plenty of tools to debug, and no need to invent another data format!. The broker validates every PUBLISH against the registered schema, so malformed messages get rejected before reaching any subscriber.</p>
<p>This would give you the compactness of a custom binary protocol with the decoupling of a schema registry. The broker is the single source of truth, and clients are as thin as possible.</p>
<h2 id="why-ziglang">Why Ziglang?</h2><p>I picked Zig for a few reasons, and most of them are practical rather than ideological, for sure.</p><ul><li>The YouTube video I saw comparing Rust vs Zig vs Go for the same workload showed how good Zig was at memory consumption. This already excited me. After searching about the language a bit more, I found that it gives you the capability to explicitly control your memory.</li><li>The Zig toolchain makes it easy for anyone to cross-compile. <code>zig build -Dtarget=aarch64-linux</code> gives me a Raspberry Pi binary from my Mac. No toolchain setup, no Docker gymnastics, no fiddling with cross-compilers.</li><li>I needed to use a language with no garbage collector, goroutine scheduler or some other technology that does stuff and decreases my tool&apos;s performance.</li></ul><p>Let&apos;s admit it before going further: <strong>I was super curious about Zig language, and wanted to use it in a project to understand its limits. </strong>That&apos;s the part with ideological reasons &#x1F604;.</p><h2 id="engineering-decisions">Engineering Decisions</h2><h3 id="a-run-time-protobuf-engine">A Run-Time Protobuf Engine</h3><p>This is the part I&apos;m most proud of and most nervous about showing people.</p>
<p>I wrote the <code>.proto</code> file parser, wire format encoder, and decoder from scratch in Zig. No <code>protoc</code>, no <code>protobuf-c</code>, no <code>nanopb</code> &#x2014; nothing external.</p>
<p>Three reasons:</p>
<ol>
<li>Zero external dependencies is a design goal. The entire broker builds with <code>zig build</code> and nothing else. No system libraries, no package manager downloads, no vendored C code. I wanted to keep it that way.</li>
<li>Protobuf&apos;s standard toolchain is a static compilation step. You run <code>protoc</code>, it generates code, you compile that code into your binary. That&apos;s fine for most use cases, but I needed runtime schema loading &#x2014; drop a <code>.proto</code> file in a directory, the broker parses it at startup, done. That doesn&apos;t fit the code generation model.</li>
<li>I wanted to learn how serializers work. Writing an encoder/decoder from the wire format spec is one of those things that sounds intimidating but teaches you a lot about how data actually lives on the wire. Field tags, varint encoding, length-delimited records &#x2014; it&apos;s surprisingly elegant once you dig in.</li>
</ol>
<p>So the engine, firstly parses <code>.proto</code> files at startup (a recursive descent parser that handles <code>syntax</code>, <code>package</code>, <code>message</code>, and all scalar field types). Then it builds a field-type table for each message type. At publish time, validates the payload against the schema and decodes/encodes Protobuf wire-format bytes field by field. In the second release, the parsing and topic-mapping functionality is enabled to execute on runtime. It means that one can register new proto interfeces to broker for supporting new data types on the run.</p>
<p>It&apos;s not a complete Protobuf implementation &#x2014; I don&apos;t support nested messages, <code>oneof</code>, maps, or extensions yet. But for flat sensor telemetry messages, it handles most of the cases. If you see something missing, don&apos;t forget to create an issue ;).</p>
<h3 id="direct-implementation-of-the-network-layer">Direct Implementation of the Network Layer</h3><p>The network layer uses <code>epoll</code> on Linux and <code>kqueue</code> on macOS &#x2014; no <code>libuv</code>, no <code>tokio</code>, no abstraction layers. Part of the motivation here was personal: as someone who works close to the Linux kernel, I wanted to actually use the <code>epoll</code> interface directly rather than through three layers of indirection. Building something real on top of it is the best way to understand how it behaves under load.</p>
<p>The event loop is a single-threaded loop that:</p>
<ol>
<li>Waits for I/O readiness events</li>
<li>Reads MQTT packets from ready sockets</li>
<li>Parses the MQTT fixed header to determine packet type</li>
<li>Dispatches to the appropriate handler (CONNECT, PUBLISH, SUBSCRIBE, etc.)</li>
<li>For PUBLISH: validates the payload against the schema, then fans out to all matching subscribers</li>
</ol>
<p>Single-threaded might sound limiting, but it avoids all synchronization overhead and keeps the per-message path as short as possible. The topic broker uses a trie structure for wildcard matching, so routing a message to subscribers is O(topic depth) regardless of the number of subscriptions.</p>
<h3 id="performance-as-the-gate-keeper">Performance as the Gate-Keeper</h3><p>I take performance seriously enough that I built a dedicated benchmarking suite as part of the project. It&apos;s written in Python and runs 7 scenarios, each designed to stress a different part of the broker:</p>
<table>
<thead>
<tr>
<th>Scenario</th>
<th>What it tests</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>B1: Baseline Concurrency</strong></td>
<td>100 concurrent connections &#x2014; measures p50/p99 latency, connection time, and memory usage</td>
</tr>
<tr>
<td><strong>B2: Thundering Herd</strong></td>
<td>10,000 clients connecting simultaneously &#x2014; connection time, fan-out latency, message loss</td>
</tr>
<tr>
<td><strong>B3: Sustained Throughput</strong></td>
<td>10-minute continuous load &#x2014; throughput stability, memory growth, CPU usage, late-stage latency</td>
</tr>
<tr>
<td><strong>B4: Wildcard Explosion</strong></td>
<td>1,000 wildcard subscribers on overlapping topic trees &#x2014; matching correctness and per-match latency</td>
</tr>
<tr>
<td><strong>B5: Protobuf Load</strong></td>
<td>Protobuf validation and encoding overhead &#x2014; decoding latency, bandwidth savings, CPU cost</td>
</tr>
<tr>
<td><strong>B6: Connection Churn</strong></td>
<td>100,000 connect/disconnect cycles &#x2014; memory leaks, file descriptor leaks, error rate</td>
</tr>
<tr>
<td><strong>B7: Message Sizes</strong></td>
<td>Throughput across payload sizes from 10 bytes to 64 KB &#x2014; finding where the broker becomes I/O bound</td>
</tr>
</tbody>
</table>
<p>Each scenario has a <code>thresholds.json</code> file that defines pass/fail criteria per metric. Every threshold specifies a direction (<code>lower</code> for things like latency and memory, <code>higher</code> for throughput and connection counts), a hard <code>max</code> or <code>min</code> limit, and a <code>warn</code> level for early regression detection. At the end of each run, the suite checks every metric against its threshold and prints a summary: <code>[PASS]</code>, <code>[WARN]</code>, or <code>[FAIL]</code> per metric.</p>
<p>For example, B1&apos;s thresholds require p99 latency under 0.8 ms (warn at 0.6 ms), memory under 3.5 MB (warn at 3.0 MB), and all 100 connections to succeed. B6 requires that memory growth over 100k connection cycles stays under 10 MB (warn at 5 MB) and file descriptor leaks are zero. If any metric crosses the <code>max</code>/<code>min</code> boundary, the benchmark fails &#x2014; it&apos;s a hard gate, not a suggestion.</p>
<p>Results from two machines &#x2014; an Apple M2 Pro and a Raspberry Pi 5:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>M2 Pro</th>
<th>RPi 5</th>
</tr>
</thead>
<tbody>
<tr>
<td>Throughput (10-byte messages)</td>
<td>208k msg/s</td>
<td>147k msg/s</td>
</tr>
<tr>
<td>p99 latency (100 clients)</td>
<td>0.44 ms</td>
<td>0.13 ms</td>
</tr>
<tr>
<td>Memory (100 connections)</td>
<td>2.6 MB</td>
<td>2.5 MB</td>
</tr>
<tr>
<td>Connection churn (100k cycles)</td>
<td>0 MB leaked</td>
<td>0 MB leaked</td>
</tr>
<tr>
<td>Sustained throughput (10 min)</td>
<td>8,981 msg/s</td>
<td>9,012 msg/s</td>
</tr>
</tbody>
</table>
<p>The Raspberry Pi numbers are the ones I care about most. If the broker runs well on a $60 ARM board, it can run on many SBCs.</p>
<p>A few things I found interesting:</p>
<ul>
<li>The RPi 5 actually has lower p99 latency (0.13 ms vs 0.44 ms on M2 Pro). My theory: macOS kqueue has more scheduling jitter than Linux epoll, possibly due to QoS classes in the Darwin scheduler.</li>
<li>Sustained throughput is nearly identical on both platforms (~9k msg/s). This tells me the bottleneck is the Python test harness, not the broker.</li>
<li>Zero memory growth over 100,000 connection cycles. I&apos;m paranoid about leaks, so testing setup check this obsessively.</li>
</ul>
<h2 id="whats-next">What&apos;s next?</h2><p>The broker is functional but still early. Things on my list:</p>
<ul>
<li><strong>QoS 1 and 2</strong> &#x2014; I need to add packet acknowledgment and the retry state machine. It will be easy but important.</li>
<li><strong>Possibility to use as C-library</strong> - Since Zig is intercompatible with C, I want to benefit from it. The majority of the embedded applications are written in C, and losing chance to integrate them while Zig provides the methodology. That&apos;d be a huge loss.</li>
<li><strong>TLS</strong> &#x2014; It should be not hard (I hope), just haven&apos;t gotten to it. I&apos;m still unsure if I can still preserve &quot;no-deps&quot; strategy when TLS is there.</li>
<li><strong>Nested Protobuf messages</strong> &#x2014; The engine currently handles flat messages only. I&apos;ll wait for this feature until someone requests it.</li>
<li><strong>Multi-node federation</strong> &#x2014; According to LLMs, this should be a really cool feature. I don&apos;t see any use case (yet). I might not do it at all.</li>
</ul>
<h2 id="how-to-try-it">How to try it?</h2><p>You can easily clone the repository and use Docker container, or better, run it on your host machine if Zig is installed.</p>
<pre><code class="language-bash">git clone https://github.com/electricalgorithm/protomq.git
cd protomq
docker compose up
</code></pre>
<p>Or if you have Zig 0.15.2 installed:</p>
<pre><code class="language-bash">zig build run-server
</code></pre>
<p>The full benchmark suite, results, and methodology are in the repo under <code>benchmarks/</code>.</p>
<hr><p>If you&apos;re interested in MQTT internals, Protobuf wire format encoding, or systems programming in Zig, I&apos;d genuinely appreciate feedback on the code. I&apos;m one person working on this, and I know there&apos;s plenty of room for improvement.</p><hr><h3 id="resources-and-links">Resources and Links</h3><ul>
<li><a href="https://electricalgorithm.github.io/protomq/">ProtoMQ</a></li>
<li><a href="https://github.com/electricalgorithm/protomq">ProtoMQ on GitHub (Source Code)</a></li>
<li><a href="https://github.com/electricalgorithm/protomq/blob/main/FAQ.md">FAQ &#x2014; deployment, schema management, admin server</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Set up Mutt with Custom iCloud Domains]]></title><description><![CDATA[<p>It&apos;s a bit tricky to set the Mutt with iCloud since there&apos;s no documentation available for custom domains and their IMAP/SMTP settings. However, what Apple does, is to authenticate you through your iCloud e-mail (@icloud.com) and change the &apos;FROM&apos; section of the</p>]]></description><link>https://gyokhan.com/set-up-mutt-with-custom-icloud-domains/</link><guid isPermaLink="false">694040b17a1fcb0001ee9d8e</guid><category><![CDATA[cheatsheets]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Mon, 15 Dec 2025 17:13:43 GMT</pubDate><content:encoded><![CDATA[<p>It&apos;s a bit tricky to set the Mutt with iCloud since there&apos;s no documentation available for custom domains and their IMAP/SMTP settings. However, what Apple does, is to authenticate you through your iCloud e-mail (@icloud.com) and change the &apos;FROM&apos; section of the mail when sending. Maybe that&apos;s also the reason why we can have all @customdomain sub-mails.</p><p>Here&apos;s the neccessary and basit ~/.muttrc file:</p><figure class="kg-card kg-code-card"><pre><code># --- User Variables
set realname = &quot;{SENDER_NAME}&quot;
set from = &quot;{YOUR_CUSTOM_EMAIL}&quot;
set imap_user = &quot;{YOUR_ICLOUD_E_MAIL}@icloud.com&quot;
set imap_pass = &quot;{YOUR_APP_PASSWORD}&quot;
set smtp_pass = &quot;{YOUR_APP_PASSWORD}&quot;

# --- Connection Settings ---
# Incoming Mail (IMAP)
set folder = &quot;imaps://imap.mail.me.com:993&quot;
set spoolfile = &quot;+INBOX&quot;
set postponed = &quot;+Drafts&quot;
set record = &quot;+Sent Messages&quot;

# Outgoing Mail (SMTP)
# Note: The SMTP URL must include your username/email
set smtp_url = &quot;smtp://$imap_user@smtp.mail.me.com:587/&quot;

# --- Security &amp; Optimization ---
set ssl_force_tls = yes
set ssl_starttls = yes
set smtp_authenticators = &quot;login&quot;

# --- Basic UI Tweaks (Optional) ---
set sort = threads
set sort_aux = reverse-last-date-received
set editor = &quot;nvim&quot;</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Make sure to get your app pasword from accounts.apple.com!</span></p></figcaption></figure><p>Have fun!</p>]]></content:encoded></item><item><title><![CDATA[Building Linux-based BusyBox Distro on M-series Macs]]></title><description><![CDATA[<p>In the world of embedded systems and operating system development, relying on heavy virtualization tools like Docker Desktop or VMware can sometimes obscure what is actually happening under the hood. I wanted to build a Linux kernel from scratch (upstream) and a minimal root filesystem using BusyBox, all on my</p>]]></description><link>https://gyokhan.com/building-linux-based-busybox-distro-on-m-series-macs/</link><guid isPermaLink="false">693eb3a57a1fcb0001ee9d19</guid><category><![CDATA[linux]]></category><category><![CDATA[busybox]]></category><category><![CDATA[embedded systems]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sun, 14 Dec 2025 13:24:26 GMT</pubDate><content:encoded><![CDATA[<p>In the world of embedded systems and operating system development, relying on heavy virtualization tools like Docker Desktop or VMware can sometimes obscure what is actually happening under the hood. I wanted to build a Linux kernel from scratch (upstream) and a minimal root filesystem using BusyBox, all on my Apple Silicon.</p><p>In this post, we will explore how to use the Lima project to create a native Linux build factory, compile Torvalds&apos; upstream kernel, and create a bootable &quot;Demo OS&quot; that runs on the QEMU test bench.</p><h3 id="the-stack">The Stack</h3><p>Before diving into commands, let&apos;s briefly define our toolkit:</p><ul><li><strong>Lima (Linux Machines):</strong> A lightweight virtualization tool for macOS. It acts as our &quot;Factory,&quot; giving us a Fedora environment to compile code that macOS cannot build natively.</li><li><strong>Linux Kernel (Upstream):</strong> The core operating system, pulled directly from Linus Torvalds&apos; source tree.</li><li><strong>BusyBox:</strong> Often called the &quot;Swiss Army Knife of Embedded Linux,&quot; this provides all our userland tools (<code>ls</code>, <code>sh</code>, <code>mount</code>) in a single binary.</li><li><strong>QEMU:</strong> An emulator and virtualizer. We use it as our &quot;Test Bench&quot; to boot our custom kernel on the Mac.</li></ul><p>The ones we&apos;ll use in the host machine (Mac OS) will be Lima and QEMU. Let&apos;s install them using homebrew:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">$ brew install qemu lima</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Install Lima for having shell-access to our builder OS, and QEMU for testing virtualization.</span></p></figcaption></figure><h3 id="setting-up-the-factory-lima">Setting Up the Factory (Lima)</h3><p>We need a Linux environment to compile the kernel. I chose Fedora for its robust package management.</p><p>First, create a Fedora instance using Lima:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">$ limactl start template://fedora --name=kernel-builder --cpus=4 --memory=8
$ limactl shell kernel-builder</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Start a Fedora virtual machine and SSH into it.</span></p></figcaption></figure><p>Once inside the shell, we need to install the development tools for building Linux kernel and BusyBox. We also need specific static libraries for BusyBox later.</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">$ sudo dnf update -y

# Install standard build tools
$ sudo dnf group install development-tools
$ sudo dnf group install c-development

# Install kernel dependencies and static libraries for BusyBox
$ sudo dnf install -y ncurses-devel openssl-devel elfutils-libelf-devel dwarves bc cpio perl glibc-static libxcrypt-static</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">The dependencies are neccessary for building the Linux and BusyBox.</span></p></figcaption></figure><h3 id="building-the-kernel-linux">Building the Kernel (Linux)</h3><p>We will use the upstream source directly from git. This ensures we are working with the latest features. Because, why not?</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Go the VM&apos;s not-mounted $HOME directory.
$ cd ~

# Clone the Linux repo.
$ git clone https://github.com/torvalds/linux.git
$ cd linux

# Build the config first, then the kernel. It&apos;ll take some time.
$ make defconfig
$ make -j$(nproc)</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">We&apos;ll go with the default config, but you can set-up your own. This step will take some time.</span></p></figcaption></figure><p>After compilation, our kernel is ready at <code>arch/arm64/boot/Image</code>.</p><h3 id="building-the-userland-busybox">Building the Userland (BusyBox)</h3><p>A kernel is useless without applications. We will build a minimal OS using BusyBox version 1.36 (stable). I&apos;m particularly using this version since with others I had issues compiling on ARM.</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Go to the VM&apos;s not-mounted $HOME directory.
$ cd ~

# Clone the BusyBox repo.
$ git clone https://git.busybox.net/busybox/
$ cd busybox

# Checkout to the 1.36-stable branch.
$ git checkout origin/1_36_stable

# Build default config.
$ make defconfig</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Preparing the userland build! It&apos;s really close to have your own distro. Stay hold!</span></p></figcaption></figure><p>There are some gotchas in building BusyBox on Fedora:</p><ol><li><strong>Static Linking:</strong> We must tell BusyBox to include all libraries inside the binary so it can run standalone.</li><li><strong>Missing <code>tc</code> Support:</strong> The Traffic Control (<code>tc</code>) applet fails to build against modern kernel headers.</li><li><strong>The Menuconfig Loop:</strong> Running <code>make menuconfig</code> might fail due to missing helpers, so we will edit the config directly. If you can achieve having menuconfig, let me know to update this blog post as well!</li></ol><p>Run the following to fix these issues and build:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Enable Static Linking
$ sed -i &apos;s/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/&apos; .config

# Disable the broken &apos;tc&apos; applet
$ sed -i &apos;s/^CONFIG_TC=y/# CONFIG_TC is not set/&apos; .config

# Build and Install
$ make -j$(nproc)
$ make install</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Changing default configs, building the project and installing it.</span></p></figcaption></figure><p>This creates an <code>_install</code> directory containing our OS. We need to package this into a <code>cpio</code> archive. Note that BusyBox creates a <code>linuxrc</code> link, but modern kernels look for <code>/init</code>. We will create our own init script:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">$ cd _install
$ mkdir -p dev proc sys

# Create the startup script
$ cat &gt; init &lt;&lt;EOF
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo &quot;-----------------------------------&quot;
echo &quot;Success! Your Kernel and Demo OS are working.&quot;
echo &quot;-----------------------------------&quot;
exec /bin/sh
EOF

$ chmod +x init

# Compress into a disk image
$ find . -print0 | cpio --null -ov --format=newc | gzip &gt; ~/initramfs.cpio.gz</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Preparing the initramfs, hence, our userspace. You can change the custom boot message to something funny :)</span></p></figcaption></figure><h3 id="the-test-bench-qemu">The Test Bench (QEMU)</h3><p>Now we move the artifacts from our Linux &quot;Factory&quot; back to our Mac &quot;Test Bench.&quot;</p><p>Exit the Lima shell (<code>exit</code>) and run the following on your Mac terminal:</p><figure class="kg-card kg-code-card"><pre><code class="language-bash"># Copy files from the Lima instance
$ limactl copy kernel-builder:~/linux/arch/arm64/boot/Image .
$ limactl copy kernel-builder:~/initramfs.cpio.gz .</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Lima is really powerful on making your life easy with interaction between host and guest .</span></p></figcaption></figure><p>Finally, we boot our custom kernel. We use <code>-accel hvf</code> to leverage Apple&apos;s Hypervisor Framework for native speeds.</p><pre><code class="language-bash">$ qemu-system-aarch64 \
  -M virt \
  -accel hvf \
  -cpu host \
  -nographic \
  -kernel Image \
  -initrd initramfs.cpio.gz \
  -append &quot;console=ttyAMA0&quot;</code></pre><p>If successful, you will be greeted by your custom boot message and a <code>#</code> shell prompt. You have just built and booted a Linux distribution from scratch on Apple Silicon.</p>]]></content:encoded></item><item><title><![CDATA[Simple U-Net Explanation]]></title><description><![CDATA[<blockquote>Written by Gemini 2.5 Flash on 2025-11-22. Do not reference it for factual information. Conversation: <a href="https://gemini.google.com/share/b8d82abc0f5d">https://gemini.google.com/share/b8d82abc0f5d</a></blockquote><p>Let&apos;s dive into the fascinating world of U-Net architecture! This is a powerful neural network especially popular in biomedical image segmentation. Imagine you have an image,</p>]]></description><link>https://gyokhan.com/simple-u-net-explaination/</link><guid isPermaLink="false">6921f4287a1fcb0001ee9cff</guid><category><![CDATA[artificial intelligence]]></category><category><![CDATA[ai-written]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sat, 22 Nov 2025 17:37:44 GMT</pubDate><content:encoded><![CDATA[<blockquote>Written by Gemini 2.5 Flash on 2025-11-22. Do not reference it for factual information. Conversation: <a href="https://gemini.google.com/share/b8d82abc0f5d">https://gemini.google.com/share/b8d82abc0f5d</a></blockquote><p>Let&apos;s dive into the fascinating world of U-Net architecture! This is a powerful neural network especially popular in biomedical image segmentation. Imagine you have an image, and you want to precisely outline certain objects within it, like identifying tumor cells in a medical scan. U-Net helps you do just that!</p><p>We&apos;ll break it down piece by piece.</p><h3 id="what-is-segmentation">What is Segmentation?</h3><p>First, let&apos;s understand what we mean by &quot;segmentation.&quot; In simple terms, it&apos;s the process of dividing an image into multiple segments or regions. Each pixel in the image is assigned to a specific class.</p><p>For example, if you have an image of a cat and a dog, segmentation would label every pixel belonging to the cat as &quot;cat&quot; and every pixel belonging to the dog as &quot;dog.&quot; The background pixels would be labeled as &quot;background.&quot;</p><h3 id="why-u-net">Why U-Net?</h3><p>Before U-Net, many convolutional neural networks (CNNs) were great for classification (e.g., &quot;Is this a cat or a dog?&quot;). However, for segmentation, we need to know <em>where</em> the objects are, not just <em>what</em> they are. Traditional CNNs often downsample the image significantly, losing spatial information crucial for precise segmentation.</p><p>U-Net was designed to address this by combining information from both contracting (downsampling) and expanding (upsampling) paths.</p><h3 id="the-u-net-architecture-a-detailed-look">The U-Net Architecture: A Detailed Look</h3><p>The U-Net architecture gets its name from its characteristic &quot;U&quot; shape. It consists of two main parts:</p><ol><li><strong>The Contracting Path (Encoder):</strong> This part is responsible for capturing context. It&apos;s similar to a traditional CNN.</li><li><strong>The Expanding Path (Decoder):</strong> This part is responsible for enabling precise localization.</li></ol><figure class="kg-card kg-image-card"><img src="https://gyokhan.com/content/images/2025/11/data-src-image-30124467-6f4c-4845-988a-c7fe08ff9ae1.png" class="kg-image" alt loading="lazy" width="512" height="512"></figure><h3 id="1-the-contracting-path-encoder">1. The Contracting Path (Encoder)</h3><p>The contracting path is on the left side of the &quot;U.&quot; It follows the typical architecture of a convolutional network:</p><ul><li><strong>Repeated 3x3 Convolutions:</strong> Each step in the contracting path typically involves two 3x3 convolutional layers. These convolutions extract features from the input image.</li><li><strong>ReLU Activation:</strong> After each convolution, a Rectified Linear Unit (ReLU) activation function is applied. This introduces non-linearity, allowing the network to learn more complex patterns.</li><li><strong>2x2 Max Pooling:</strong> After the two convolutional layers, a 2x2 max-pooling operation is performed with a stride of 2. This downsamples the feature maps, effectively reducing their spatial dimensions (e.g., halving the width and height) and increasing the number of feature channels. This process helps capture higher-level, more abstract features while reducing computational cost.</li></ul><p>As we go deeper into the contracting path, the spatial resolution decreases, but the number of feature channels (depth) increases. This allows the network to learn more complex and abstract representations of the input image.</p><h3 id="2-the-expanding-path-decoder">2. The Expanding Path (Decoder)</h3><p>The expanding path is on the right side of the &quot;U.&quot; Its goal is to take the learned abstract features and progressively upsample them to the original image resolution, while also incorporating fine-grained details lost during downsampling.</p><ul><li><strong>Upsampling (Up-Convolution/Transpose Convolution):</strong> Each step begins with an upsampling of the feature map. This is often done using a 2x2 &quot;up-convolution&quot; (also known as a transpose convolution or deconvolution) with a stride of 2. This effectively doubles the spatial dimensions of the feature map.</li><li><strong>Concatenation with Skip Connections:</strong> This is where the &quot;U&quot; shape becomes crucial! After upsampling, the feature map is concatenated (joined) with a corresponding feature map from the contracting path. These are called <strong>skip connections</strong>.<ul><li><strong>Why skip connections?</strong> The contracting path loses spatial information during downsampling. Skip connections bring back this lost fine-grained detail from the corresponding level in the contracting path. This allows the expanding path to recover precise localization information, which is vital for accurate segmentation.</li></ul></li><li><strong>Repeated 3x3 Convolutions:</strong> After concatenation, two 3x3 convolutional layers are applied, followed by ReLU activations. These convolutions refine the features by integrating the upsampled and concatenated information.</li></ul><h3 id="the-bottleneck">The Bottleneck</h3><p>At the very bottom of the &quot;U,&quot; connecting the contracting and expanding paths, there&apos;s a &quot;bottleneck&quot; section. This usually consists of two 3x3 convolutional layers, similar to the other blocks, further processing the most abstract features before the upsampling process begins.</p><h3 id="the-output-layer">The Output Layer</h3><p>Finally, at the end of the expanding path, a 1x1 convolutional layer is used. This layer maps the feature channels to the desired number of classes for segmentation. For example, if you want to segment three classes (e.g., tumor, healthy tissue, background), this layer will output a feature map with three channels. Each channel represents the probability of a pixel belonging to that specific class.</p><h3 id="how-it-all-comes-together">How it all comes together</h3><p>Imagine tracing a pixel&apos;s journey:</p><ol><li><strong>Downsampling:</strong> As the pixel information goes down the contracting path, it loses its precise location but gains rich contextual information about its surroundings and the larger structures in the image.</li><li><strong>Bottleneck:</strong> The information is highly abstract here, representing the most compressed and feature-rich representation.</li><li><strong>Upsampling and Skip Connections:</strong> As the information goes up the expanding path, it starts regaining its spatial resolution. Crucially, at each step, it&apos;s combined with high-resolution features from the corresponding level of the contracting path (via skip connections). This means the network can use both the rich context from the deep layers <em>and</em> the precise spatial details from the shallow layers to make its segmentation decision.</li></ol><p>This combination of context and localization is what makes U-Net so effective for pixel-wise segmentation tasks.</p><h3 id="advantages-of-u-net">Advantages of U-Net</h3><ul><li><strong>Precise Localization:</strong> The skip connections allow the network to achieve very precise segmentation boundaries.</li><li><strong>Small Dataset Efficiency:</strong> U-Net was initially designed for biomedical imaging, where training data can be limited. Its architecture allows it to perform well even with relatively small datasets, thanks to extensive data augmentation.</li><li><strong>End-to-End Segmentation:</strong> It performs segmentation in one go, without requiring multiple stages or complex post-processing.</li></ul><h3 id="applications">Applications</h3><p>While famously used in biomedical image segmentation (e.g., segmenting cells, organs, or tumors), U-Net has found applications in various other fields:</p><ul><li><strong>Satellite Image Analysis:</strong> Identifying roads, buildings, or agricultural fields.</li><li><strong>Autonomous Driving:</strong> Segmenting pedestrians, vehicles, and road signs.</li><li><strong>Material Science:</strong> Analyzing microstructures.</li></ul><h3 id="conclusion">Conclusion</h3><p>U-Net is a brilliant piece of architecture that elegantly solves the challenge of image segmentation by skillfully combining downsampling for context extraction and upsampling with skip connections for precise localization. Its &quot;U&quot; shape is not just aesthetic; it&apos;s a functional design that has revolutionized pixel-wise prediction tasks in various domains.</p><p>Keep exploring, and you&apos;ll find even more fascinating ways neural networks are shaping our world!</p>]]></content:encoded></item><item><title><![CDATA[Vibe-Coding a Hologram Generator]]></title><description><![CDATA[<p>If you&apos;ve ever fiddled with inline holography, you know the &#x201C;twin image&#x201D; is the little horror movie that always plays during reconstruction. You try to pull a clean object out of an interference pattern and&#x2014;boom&#x2014;an unwanted mirror-ghost shows up, smearing things and</p>]]></description><link>https://gyokhan.com/vibe-coding-a-hologram-generator/</link><guid isPermaLink="false">6919221e7a1fcb0001ee9ccc</guid><category><![CDATA[holography]]></category><category><![CDATA[ai-written]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sun, 16 Nov 2025 01:11:57 GMT</pubDate><media:content url="https://gyokhan.com/content/images/2025/11/hologen-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://gyokhan.com/content/images/2025/11/hologen-1.png" alt="Vibe-Coding a Hologram Generator"><p>If you&apos;ve ever fiddled with inline holography, you know the &#x201C;twin image&#x201D; is the little horror movie that always plays during reconstruction. You try to pull a clean object out of an interference pattern and&#x2014;boom&#x2014;an unwanted mirror-ghost shows up, smearing things and ruining your metrics. What if we could train a neural network to hush that ghost? Better yet: what if we could churn out thousands of realistic synthetic holograms without living in the lab for six months?<br><br>Meet `HoloGen` &#x2014; a Python toolkit I built by vibe-coding with an AI pair-programmer. It generates synthetic hologram datasets so you can train ML models to suppress twin images, test reconstruction methods, or just amuse yourself with simulated speckle patterns. And yes, it&#x2019;s open source.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/electricalgorithm/hologen"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - electricalgorithm/hologen: The HoloGen toolkit generates paired object-domain images and their inline or off-axis holograms for machine learning workflows.</div><div class="kg-bookmark-description">The HoloGen toolkit generates paired object-domain images and their inline or off-axis holograms for machine learning workflows. - electricalgorithm/hologen</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt="Vibe-Coding a Hologram Generator"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">electricalgorithm</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/27179dd07ac4ed0007bc80ff680fbf0489d36b88185322c2bbdff244459d02f6/electricalgorithm/hologen" alt="Vibe-Coding a Hologram Generator"></div></a></figure><h3 id="the-twin-image-problem-aka-why-holography-has-a-stalker">The Twin Image Problem (aka Why Holography Has a Stalker)</h3><p>Quick primer: inline holography records interference between light scattered by an object and the unscattered reference beam. One-shot, simple optics&#x2014;very elegant. The catch: when you numerically reconstruct, you get two overlapping images: the real object and its conjugate twin. They overlap, blur, and make accurate analysis annoyingly difficult.<br><br>Classical fixes: iterative phase retrieval, off-axis setups (more hardware), or clever filtering. These work, but they&#x2019;re either slow, hardware-heavy, or finicky. What if a physics-aware neural network could learn to remove the conjugate while leaving the true image intact?</p><h3 id="physics-aware-swin-transformers-to-the-rescue">Physics-Aware SWIN Transformers to the Rescue</h3><p>The plan: train Shifted Window (SWIN) transformers that don&#x2019;t just learn pixel mappings but actually respect wave physics. These models need to learn Fresnel propagation, how twin images form, and how to suppress the conjugate while preserving phase and amplitude information.<br><br>Big problem: they need a ton of paired data &#x2014; object &#x2194; hologram &#x2014; with varied noise, aberrations, distances, etc. Collecting that in the real world is slow and expensive.</p><h3 id="vibe-coding-the-dataset-generator">Vibe-Coding the Dataset Generator</h3><pre><code class="language-python">from hologen import (
    HologramConfig,
    InlineHolography,
    CircleGenerator,
    SensorNoiseModel,
    generate_hologram
)

# Generate a hologram with realistic sensor noise
config = HologramConfig(
    image_size=512,
    pixel_size=3.45e-6,  # microns
    wavelength=532e-9,   # green laser
    propagation_distance=0.01  # 1 cm
)

obj_field = CircleGenerator(radius=50).generate(config)
hologram = generate_hologram(obj_field, config, InlineHolography())

# Add sensor noise (shot noise, read noise, quantization)
noise_model = SensorNoiseModel(
    read_noise_std=5.0,
    dark_current_rate=0.1,
    quantum_efficiency=0.8
)
noisy_hologram = noise_model.apply(hologram, config)</code></pre><h3 id="public-dataset-%E2%80%94-intensity-only-inline-holograms-100k-pairs-%E2%89%88157-gb">Public dataset &#x2014; intensity-only inline holograms (100k pairs, &#x2248;157 GB)</h3><p>Good news: you don&#x2019;t have to start from scratch. There&#x2019;s already a large, published dataset of intensity-only inline holograms &#x2014; about 100,000 paired examples (roughly 157 GB) &#x2014; hosted on Hugging Face. Think of it as a 157 GB buffet of holographic snacks: perfect as a training baseline or to augment HoloGen&#x2019;s synthetic data. Note that this dataset provides intensity-only pairs (no complex phase ground truth), so if your SWIN model expects complex-field targets you&#x2019;ll either need to reconstruct phase from the intensities or pair these real examples with HoloGen&#x2019;s phase-aware simulations for a hybrid training strategy.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://huggingface.co/datasets/gokhankocmarli/inline-digital-holography"><div class="kg-bookmark-content"><div class="kg-bookmark-title">gokhankocmarli/inline-digital-holography &#xB7; Datasets at Hugging Face</div><div class="kg-bookmark-description">We&#x2019;re on a journey to advance and democratize artificial intelligence through open source and open science.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://huggingface.co/favicon.ico" alt="Vibe-Coding a Hologram Generator"></div></div><div class="kg-bookmark-thumbnail"><img src="https://cdn-thumbnails.huggingface.co/social-thumbnails/datasets/gokhankocmarli/inline-digital-holography.png" alt="Vibe-Coding a Hologram Generator"></div></a></figure><h3 id="coming-soon">Coming Soon</h3><p>Next step: train SWIN transformers on HoloGen datasets. Input: noisy inline holograms. Output: clean reconstructions with the twin suppressed. Key idea: make the network physics-aware &#x2014; include the propagation operator in the loss so the model learns to respect the wave equation.</p><h3 id="vibe-coding-the-future-of-research-software">Vibe-Coding: the Future of Research Software?</h3><p>Building HoloGen via AI-assisted vibe-coding was illuminating. The usual cycle &#x2014; prototype, curse, rewrite, repeat &#x2014; compresses into faster, iterative loops. I focused on physics and architecture; the AI helped with boilerplate, patterns, and catching dumb bugs.</p><p>Is it perfect? No. Is it usable, tested, and extensible? Absolutely. More importantly: it exists and works &#x2014; unlike many research prototypes that die in notebooks.</p><p>The twin image has haunted holography since Gabor&#x2019;s day (1948). Maybe neural nets can finally tell it to take a hike. And maybe vibe-coding is the shortcut that gets us there sooner &#x2014; with fewer late-night lab runs and more coffee-fueled model training.</p>]]></content:encoded></item><item><title><![CDATA[Podcast: Developing with Zephyr RTOS]]></title><description><![CDATA[<figure class="kg-card kg-embed-card"><iframe width="100%" height="400" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?visual=true&amp;url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F2044447016&amp;show_artwork=true"></iframe></figure><blockquote>Disclaimer: This podcast is generated by an artificial intelligence.</blockquote>]]></description><link>https://gyokhan.com/podcast-developing-with-zephyr-rtos/</link><guid isPermaLink="false">67c246a5db6c310001b05138</guid><category><![CDATA[embedded systems]]></category><category><![CDATA[podcast]]></category><category><![CDATA[ai-written]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Fri, 28 Feb 2025 23:29:34 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-embed-card"><iframe width="100%" height="400" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?visual=true&amp;url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F2044447016&amp;show_artwork=true"></iframe></figure><blockquote>Disclaimer: This podcast is generated by an artificial intelligence.</blockquote>]]></content:encoded></item><item><title><![CDATA[Bookmarks for Building Yocto on Apple Silicon]]></title><description><![CDATA[<figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://ejaaskel.dev/how-to-build-yocto-with-apple-silicon/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">How to build Yocto with Apple Silicon - Esa Jaaskela</div><div class="kg-bookmark-description">If you&#x2019;re like me, you have more money than brains and not too much of either really. This can lead to situations where you end up buying a MacBook without actually checking if it supports Yocto builds, the</div></div></a></figure>]]></description><link>https://gyokhan.com/bookmarks-for-building-yocto-on-apple-silicon/</link><guid isPermaLink="false">677aa2caf92d650001ebb2ef</guid><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sun, 05 Jan 2025 15:19:02 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://ejaaskel.dev/how-to-build-yocto-with-apple-silicon/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">How to build Yocto with Apple Silicon - Esa Jaaskela</div><div class="kg-bookmark-description">If you&#x2019;re like me, you have more money than brains and not too much of either really. This can lead to situations where you end up buying a MacBook without actually checking if it supports Yocto builds, the one thing you inexplicably like to waste your little free time on. As it turns out after</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://ejaaskel.dev/wp-content/uploads/2023/11/cropped-ejaaskel_logo-1-300x300.png" alt><span class="kg-bookmark-author">Esa Jaaskela - Embedded Software Handyman</span><span class="kg-bookmark-publisher">ejaaskel</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://ejaaskel.dev/wp-content/uploads/2023/03/majava-scaled.jpg" alt></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/crops/docker-win-mac-docs/wiki/Mac-Instructions"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Mac Instructions</div><div class="kg-bookmark-description">The wiki explains how to use docker containers created to run Yocto Project tools in a Windows or Mac environment. - crops/docker-win-mac-docs</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">crops</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/81de717bc15bb5aee41e4df51ab755645c4b5482ba523df0f090fdb89d492445/crops/docker-win-mac-docs" alt></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Podcast: Introduction to Yocto Project]]></title><description><![CDATA[<p>This podcast for the Yocto Project, a framework for building embedded Linux systems, details its development environment, build system concepts (including BitBake and the OpenEmbedded build system), Git workflow for contributing changes, and numerous classes and variables used in recipe creation and configuration. It also covers image creation, SDK generation,</p>]]></description><link>https://gyokhan.com/podcast-introduction-to-yocto-project/</link><guid isPermaLink="false">677a9095f92d650001ebb2d3</guid><category><![CDATA[embedded systems]]></category><category><![CDATA[podcast]]></category><category><![CDATA[ai-written]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sun, 05 Jan 2025 14:14:10 GMT</pubDate><content:encoded><![CDATA[<p>This podcast for the Yocto Project, a framework for building embedded Linux systems, details its development environment, build system concepts (including BitBake and the OpenEmbedded build system), Git workflow for contributing changes, and numerous classes and variables used in recipe creation and configuration. It also covers image creation, SDK generation, testing methodologies (including QEMU emulation and various testing frameworks), and troubleshooting QA errors and warnings. Finally, it provides migration guides for upgrading between Yocto Project releases and notes on removed or deprecated features.</p><figure class="kg-card kg-embed-card"><iframe width="100%" height="400" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?visual=true&amp;url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F2001555519&amp;show_artwork=true"></iframe></figure><blockquote>Disclaimer: This podcast is generated by an artificial intelligence.</blockquote>]]></content:encoded></item><item><title><![CDATA[Docker Compose Files' Location of CasaOS]]></title><description><![CDATA[<p>I was using the CasaOS for my AdGuard, NextCloud and Ollama services. After having a problem with its mounting strategy (It uses devmon, and changes the user rights of the mount point &#x2013; therefore, the common user cannot access the drives.) and destroying my portable HDD consisting of 12 years</p>]]></description><link>https://gyokhan.com/docker-compose-files-location-of-casaos/</link><guid isPermaLink="false">66c20833b7110f0001fa73ce</guid><category><![CDATA[cheatsheets]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sun, 18 Aug 2024 12:48:30 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1697058149199-bc6d94639a82?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDl8fGRvY2tlcnxlbnwwfHx8fDE3MjM5ODUxMjN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1697058149199-bc6d94639a82?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDl8fGRvY2tlcnxlbnwwfHx8fDE3MjM5ODUxMjN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Docker Compose Files&apos; Location of CasaOS"><p>I was using the CasaOS for my AdGuard, NextCloud and Ollama services. After having a problem with its mounting strategy (It uses devmon, and changes the user rights of the mount point &#x2013; therefore, the common user cannot access the drives.) and destroying my portable HDD consisting of 12 years memories, I&apos;ve decided to remove it from my system and use the simple Docker and Docker Compose.</p><p>The removal process was quite easy and I was enough brave after losing all the backups I had. The next step was quite clear: I needed to make sure all the <code>docker-compose.yml</code> are available, so that I can customize them whenever I need. </p><p>This document will provide you with where you can find your docker-compose configuration files. That&apos;s it. Plus, I just wanted to shout out that my drive has become garbage just because I used that CasaOS magic.</p><pre><code>/var/lib/casaos/apps</code></pre><p>Just go to that directory, and look for the app you&apos;d like to access its compose definition.</p><p>Thanks.</p>]]></content:encoded></item><item><title><![CDATA[Enabling HTTPS for NextCloud on CasaOS]]></title><description><![CDATA[<ol><li>Please add the following env var into NextCloud&apos;s Docker Compose: <code>NEXTCLOUD_OVERWRITEPROTOCOL=https</code></li><li>Afterwards, go to NextCloud&apos;s directory (which you can find using the app&apos;s settings, and check the volume mount point) and edit the <code>config/config.php</code> file.</li><li>Insert <code>overwriteprotocol&apos; =&gt; &apos;</code></li></ol>]]></description><link>https://gyokhan.com/enabling-https-for-nextcloud-on-casaos/</link><guid isPermaLink="false">66c20833b7110f0001fa73cd</guid><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sat, 17 Aug 2024 19:26:45 GMT</pubDate><content:encoded><![CDATA[<ol><li>Please add the following env var into NextCloud&apos;s Docker Compose: <code>NEXTCLOUD_OVERWRITEPROTOCOL=https</code></li><li>Afterwards, go to NextCloud&apos;s directory (which you can find using the app&apos;s settings, and check the volume mount point) and edit the <code>config/config.php</code> file.</li><li>Insert <code>overwriteprotocol&apos; =&gt; &apos;https&apos;,</code> anywhere you&apos;d like.</li><li>Also, add your domain name to <code>trusted_domains</code> attribute, and change the <code>overwrite.cli.url</code> to the same domain name.</li></ol><p>Afterwards, re-start the app. Now, you can reach to your NextCloud instance using the new domain.</p>]]></content:encoded></item><item><title><![CDATA[Bachelor's Thesis: Developing of a Performance Comparison Index for Image Super-Resolution Models]]></title><description><![CDATA[<p>I had been working on my thesis for a year after finishing all my studies and participated in a great research journey within the Holographic Imagining Laboratory of Marmara University. We have asked the ultimate question of image processing: how to determine a picture&apos;s quality? Here in this</p>]]></description><link>https://gyokhan.com/bachelors-thesis/</link><guid isPermaLink="false">66c20833b7110f0001fa73cc</guid><category><![CDATA[lang::en]]></category><category><![CDATA[artificial intelligence]]></category><category><![CDATA[egitim]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sun, 04 Aug 2024 22:54:07 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1607647074969-cc58ec404eb2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDE3fHxncmFkdWF0ZXxlbnwwfHx8fDE3MjM5ODQ1ODV8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1607647074969-cc58ec404eb2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDE3fHxncmFkdWF0ZXxlbnwwfHx8fDE3MjM5ODQ1ODV8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Bachelor&apos;s Thesis: Developing of a Performance Comparison Index for Image Super-Resolution Models"><p>I had been working on my thesis for a year after finishing all my studies and participated in a great research journey within the Holographic Imagining Laboratory of Marmara University. We have asked the ultimate question of image processing: how to determine a picture&apos;s quality? Here in this thesis and the published article, you can find our solution to this dilemma. </p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://link.springer.com/article/10.1007/s11760-024-03430-8"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Performance comparison index for image super-resolution models - Signal, Image and Video Processing</div><div class="kg-bookmark-description">Image super-resolution is a critical aspect of image enhancement, facilitating the reconstruction of high-quality images from low-resolution inputs. Traditional quality assessment metrics like SSIM, MSE, and PSNR have limitations in effectively evaluating super-resolution models due to their focus o&#x2026;</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://link.springer.com/oscar-static/img/favicons/darwin/android-chrome-192x192-6f081ca7e5.png" alt="Bachelor&apos;s Thesis: Developing of a Performance Comparison Index for Image Super-Resolution Models"><span class="kg-bookmark-author">SpringerLink</span><span class="kg-bookmark-publisher">G&#xF6;khan Ko&#xE7;marl&#x131;</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://static-content.springer.com/image/art%3A10.1007%2Fs11760-024-03430-8/MediaObjects/11760_2024_3430_Fig1_HTML.png" alt="Bachelor&apos;s Thesis: Developing of a Performance Comparison Index for Image Super-Resolution Models"></div></a><figcaption>The shorter version of the thesis, we have published an article in Springer&apos;s Signal Image and Video Processing journal.</figcaption></figure><p>The files related to the thesis:</p>
        <div class="kg-card kg-file-card kg-file-card-medium">
            <a class="kg-file-card-container" href="https://gyokhan.com/content/files/2024/08/Presentation.pdf" title="Download" download>
                <div class="kg-file-card-contents">
                    <div class="kg-file-card-title">Bachelor&apos;s Thesis - Presentation</div>
                    
                    <div class="kg-file-card-metadata">
                        <div class="kg-file-card-filename">Presentation.pdf</div>
                        <div class="kg-file-card-filesize">13 MB</div>
                    </div>
                </div>
                <div class="kg-file-card-icon">
                    <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"/><line class="a" x1="12" y1="6.75" x2="12" y2="18"/><circle class="a" cx="12" cy="12" r="11.25"/></svg>
                </div>
            </a>
        </div>
        
        <div class="kg-card kg-file-card kg-file-card-medium">
            <a class="kg-file-card-container" href="https://gyokhan.com/content/files/2024/08/Thesis.pdf" title="Download" download>
                <div class="kg-file-card-contents">
                    <div class="kg-file-card-title">Bachelor&apos;s Thesis - The Thesis </div>
                    
                    <div class="kg-file-card-metadata">
                        <div class="kg-file-card-filename">Thesis.pdf</div>
                        <div class="kg-file-card-filesize">28 MB</div>
                    </div>
                </div>
                <div class="kg-file-card-icon">
                    <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"/><line class="a" x1="12" y1="6.75" x2="12" y2="18"/><circle class="a" cx="12" cy="12" r="11.25"/></svg>
                </div>
            </a>
        </div>
        ]]></content:encoded></item><item><title><![CDATA[Converting HEIC to PNG and Reducing Size]]></title><description><![CDATA[<p>In today&apos;s fast-paced digital world, the need for efficient and seamless media conversion has become more important than ever. Apple&apos;s macOS offers an inbuilt tool called &quot;sips&quot; that simplifies the process of converting files between various formats. In this post, we will explore how</p>]]></description><link>https://gyokhan.com/converting-heic-to-png-and-reducing-its-size/</link><guid isPermaLink="false">66c20833b7110f0001fa73cb</guid><category><![CDATA[cheatsheets]]></category><category><![CDATA[lang::en]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Sat, 08 Jun 2024 21:48:26 GMT</pubDate><content:encoded><![CDATA[<p>In today&apos;s fast-paced digital world, the need for efficient and seamless media conversion has become more important than ever. Apple&apos;s macOS offers an inbuilt tool called &quot;sips&quot; that simplifies the process of converting files between various formats. In this post, we will explore how to use the sips tool to convert formats, optimize image compression using pngquant, and why it&apos;s beneficial to handle media conversions locally on your Macbook.</p><h2 id="part-1-understanding-sipsthe-swiss-army-knife-of-media-conversion-tools">Part 1: Understanding Sips - The Swiss Army Knife of Media Conversion Tools</h2><p>The sips (Image Converter) tool, bundled with macOS, provides a simple yet powerful command-line interface for converting images and videos between different formats. It not only supports popular formats like JPEG, PNG, TIFF, and PDF but also offers a range of customization options to fine-tune your conversions.</p><p>To get an overview of the output formats supported by sips, simply run the following command in Terminal:</p><pre><code class="language-zsh">sips --formats</code></pre><p>Let&apos;s use sips to convert our HEIC file to PNG in the following command.</p><pre><code class="language-zsh">sips -s format png IMAGE_NAME.HEIC --out IMAGE_NAME.png</code></pre><h2 id="part-2-optimize-image-compression-with-pngquant">Part 2: Optimize Image Compression with Pngquant</h2><p>When dealing with images, file size can be a significant concern, especially when transferring files or working with limited storage. One way to reduce the size of your PNG images without sacrificing quality is by utilizing pngquant - an advanced PNG optimization tool.</p><p>To install pngquant on your Macbook, follow these steps:</p><ol><li>Open Terminal.</li><li>Run the following command:</li></ol><figure class="kg-card kg-code-card"><pre><code class="language-zsh">brew install pngquant</code></pre><figcaption>Make sure you have Homebrew installed before proceeding with this step.</figcaption></figure><p>3. Once the installation is complete, you can use pngquant to compress your PNG images by specifying a quality rate between 0 (lossless compression) and 100 (maximum compression). Here&apos;s an example:</p><pre><code class="language-zsh">pngquant IMAGE_NAME.png --quality 75</code></pre><p>In this example, the image &quot;IMAGE_NAME.png&quot; will be compressed to &quot;IMAGE_NAME-fs8.png&quot; with a quality setting of 75. Adjust the quality parameter according to your requirements and the desired level of visual quality.</p><hr><p>Media conversion is an essential aspect of digital workflows, and macOS provides users with an intuitive solution through its built-in sips tool. By leveraging sips and complementary tools like pngquant, users can streamline their media conversion processes, optimize file sizes, and avoid the inconvenience of uploading files to servers for conversion. So, why wait? Give sips a try today and experience the simplicity and efficiency of local media conversion on your Macbook!</p><blockquote>Written by G&#xF6;khan, edited by Hermes AI.</blockquote>]]></content:encoded></item><item><title><![CDATA[.cmake files on Modern CMake]]></title><description><![CDATA[<blockquote>This post is written by LLaMa3-70b model.</blockquote><p>In Modern CMake, configuration files, also referred to as &quot;config modules&quot; or &quot;package config files,&quot; play a vital role in managing dependencies and facilitating package discovery.</p><p>A configuration file is a CMake script generated by a package (e.g.</p>]]></description><link>https://gyokhan.com/cmake-files-on-modern-cmake/</link><guid isPermaLink="false">66c20833b7110f0001fa73ca</guid><category><![CDATA[cpp]]></category><category><![CDATA[lang::en]]></category><category><![CDATA[ai-written]]></category><dc:creator><![CDATA[Gyokhan Kochmarla]]></dc:creator><pubDate>Fri, 26 Apr 2024 07:04:01 GMT</pubDate><content:encoded><![CDATA[<blockquote>This post is written by LLaMa3-70b model.</blockquote><p>In Modern CMake, configuration files, also referred to as &quot;config modules&quot; or &quot;package config files,&quot; play a vital role in managing dependencies and facilitating package discovery.</p><p>A configuration file is a CMake script generated by a package (e.g., a library) to provide essential information about its installation. Typically, these files have a <code>.cmake</code> extension and are named after the package, such as <code>MyPackageConfig.cmake</code>.</p><p>The primary objectives of a configuration file are to:</p><ol><li><strong>Declare package availability</strong>: The configuration file notifies CMake of the package&apos;s installation and readiness for use.</li><li><strong>Provide package metadata</strong>: The file contains crucial information about the package, including its version, include directories, library paths, and other relevant details.</li><li><strong>Simplify dependency management</strong>: By incorporating the configuration file into a CMake project, you can effortlessly utilize the package without manually specifying required include directories, libraries, and compiler flags.</li></ol><p>Upon package installation, the configuration file is typically placed in a standard location, such as <code>/usr/lib/cmake/MyPackage</code> (on Linux) or <code>C:\Program Files\CMake\MyPackage</code> (on Windows).</p><p>To utilize a package in a CMake project, you would typically include the configuration file using the <code>find_package</code> command, as follows:</p><pre><code class="language-cmake">find_package(MyPackage REQUIRED)
</code></pre><p>CMake will then search for the <code>MyPackageConfig.cmake</code> file in the standard locations and incorporate it into the project. The configuration file will set various CMake variables, such as <code>MyPackage_INCLUDE_DIRS</code> and <code>MyPackage_LIBRARIES</code>, which can be used to link against the package.</p><p>In Modern CMake, configuration files are an integral component of the &quot;Config-file Packages&quot; mechanism, providing a standardized approach for packages to interact with CMake. By leveraging configuration files, you can efficiently manage dependencies and create more modular and reusable CMake projects.</p><blockquote>Written by LLaMa3-70b-8192.</blockquote>]]></content:encoded></item></channel></rss>