Skip to main content
Background Image
  1. Projects/
  2. WindRPC/

WindRPC Devlog 1 - Project Origins

·5 mins· loading · loading ·
WindRPC - This article is part of a series.
Part : This Article

Project Origins
#

This is the first devlog in the development story of WindRPC (Micro Interconnect & Network Dispatch), a lightweight Remote Procedure Call (RPC) framework designed for communication between microcontrollers and host applications.

https://github.com/micro-artwork/windrpc


Background
#

While developing the Bitnari project (an ambient light controller), I was evaluating various communication protocols for inter-device messaging.

My goal was to apply Protocol Buffers (Protobuf) for structured binary data serialization between controller hardware and PC applications.

While I preferred an RPC-style function execution model over raw message passing, standard gRPC requires heavy HTTP/2 infrastructure, making it impossible to run directly on resource-constrained microcontrollers (MCUs) using NanoPB.

Initially, I considered a simple compromise: using Protobuf strictly as a data message payload format without an RPC abstraction.

Initial Concept & GATT Inspiration
#

However, I still wanted an RPC-like mechanism rather than simple message passing. Driven by technical curiosity, I decided to design a lightweight RPC system inspired by the Bluetooth Low Energy (BLE) GATT architecture.

BLE GATT organizes communication around Profiles, Services, and Characteristics, assigning operational properties (Read, Write, Write Without Response, Notify, Indicate) to characteristics.

While attempting to adapt this concept, deeper analysis revealed that the BLE GATT model is not well-suited for true bidirectional RPC:

  • Attribute-Centric Limitation: BLE characteristics are designed primarily for reading/writing device state variables or emitting unidirectional events.
  • Difficulty in Transaction Mapping: It lacks a native pattern for mapping request packets to response packets asynchronously using transaction sequence IDs.

Rather than adopting BLE’s attribute model directly, I borrowed its core ideas: hierarchical organization (Services) and operation property classification.

Hierarchical Structure
#

  • Replace data variables under Services with explicit Procedure definitions.

Procedure Property Classification
#

  • Request-Response: Standard bidirectional RPC where the client sends a request and waits for a server response.
  • One-Way (Write Without Response): Fast command transmission without waiting for confirmation (e.g., high-speed LED pixel streaming).
  • Asynchronous Notification (Notify): Server-initiated data push when sensor values change or device events occur, without explicit client polling.

The Missing RPC Descriptor
#

After establishing the operation types, I encountered another major hurdle: the lack of a lightweight RPC specification descriptor format.

  • Standard .proto gRPC Syntax Unusable: Standard .proto files use service and rpc keywords tailored specifically for gRPC, which cannot be parsed by NanoPB in embedded environments. Reusing gRPC syntax with a custom parser risked causing syntax confusion.
  • Custom IDL Cost: Designing a brand-new Interface Definition Language (IDL) and parser from scratch required prohibitive maintenance overhead.

YAML-Based RPC Descriptor (user_spec.yml)
#

To solve this, I adopted YAML as the specification descriptor format. YAML is human-readable, easy to edit, and trivial to parse into Python objects.

Instead of writing .proto files, C headers, and C# classes manually, a single user_spec.yml file serves as the source of truth for the entire interface.

Separation of Core Spec and User Spec
#

A key architectural design decision was strictly separating the Core Spec from the User Spec.

If framework-level system services (such as Ping and Device Info) were mixed with application-specific business logic (such as Wi-Fi settings or LED streaming), framework updates could cause RPC ID collisions or maintenance headaches.

Therefore, WindRPC divides specifications into two distinct scopes:

  • Core Spec: Essential framework-level services. Service IDs 1 through 6 are reserved for system RPCs (e.g., Ping, Version Handshake) and shared data types/enums.
  • User Spec: Application-specific logic defined by developers. Developers use Service IDs 7 and above, defining custom logic inside user_spec.yml.

This separation guarantees backward compatibility and framework independence while allowing developers to focus solely on their application specification.

user_spec.yml Specification Rules
#

  1. Metadata: Define project name (project) and spec version (version).
  2. Enums: Define status codes and constant enumerations.
  3. Structs: Define message structures with field types (string, uint32, bytes) and static memory constraints (max_length, max_count).
  4. Services & RPCs:
    • Service ID: Unique 16-bit namespace identifier (User range: 7+).
    • RPC ID: Unique function identifier within a service.
    • Mapping: Assign request, response, or event message targets.
# Example user_spec.yml structure
package: bitnari

info:
  title: "Bitnari Control Specification"
  version: "1.0.0"

services:
  - id: 7
    name: led_control
    messages:
      - name: PixelColor
        fields:
          - { number: 1, name: r, type: uint32 }
          - { number: 2, name: g, type: uint32 }
          - { number: 3, name: b, type: uint32 }
      - name: PixelData
        fields:
          - number: 1
            name: colors
            type: PixelColor
            property: repeated
            nanopb: { max_count: 64 }
    rpcs:
      - id: 1
        name: display_pixels
        type: REQUEST_ONLY
        request: PixelData

  - id: 8
    name: power_manager
    messages:
      - name: PowerStatus
        fields:
          - { number: 1, name: voltage_mv, type: uint32 }
          - { number: 2, name: is_charging, type: bool }
    rpcs:
      - id: 1
        name: get_power_status
        type: REQUEST_RESPONSE
        request: types.Empty
        response: PowerStatus
      - id: 2
        name: charging_alert
        type: NOTIFICATION
        event: PowerStatus

Verification & Proto Generation
#

With the descriptor format established, I wrote the parser generator to output standard .proto files from user_spec.yml.

Initially, the primary goal was verifying that valid .proto files were consistently generated from the YAML descriptor.

Using the generated .proto files, I compiled NanoPB C code for MCU and C# classes for .NET, verifying end-to-end binary packet serialization and deserialization across host and embedded environments.

Confirming error-free binary exchange between C MCU and C# .NET validated the descriptor-driven code generation architecture.

Automatic Nanopb .options Generation
#

Alongside .proto files, the generator script was built to automatically emit Nanopb .options files (e.g., user_service.options).

This was essential due to the memory constraints of MCU C environments:

  • Zero-Heap Static Memory: Standard Protobuf string or repeated fields require dynamic memory allocation (malloc/free) or pb_callback_t decoding callbacks in C. In RAM-restricted MCUs, dynamic allocation risks memory fragmentation and runtime crashes.
  • Automatic Extraction of nanopb: Bounds: By declaring static bounds in YAML (nanopb: { max_size: 64, max_count: 32 }), the generator emits matching .options rules automatically.
  • Static C Struct Generation: The Nanopb compiler (protoc) uses the .options file to replace dynamic pointers and field callbacks with fixed static C arrays (e.g., char buffer[64];, uint32_t colors[32];).

As a result, developers obtain Zero-Heap static C structures directly from YAML definitions without writing manual .options files or C headers.


Conclusion
#

What started as a desire to use Protocol Buffers in the Bitnari project evolved into designing a custom RPC framework.

In the next post, I will discuss the early nested message structure and C server code generation.

WindRPC - This article is part of a series.
Part : This Article

Related

XInput to DualShock 4 (XI2DS)
·2 mins· loading · loading
XInput to Dualshock 4 (XI2DS) # https://github.com/micro-artwork/xi2ds About # The XI2DS is designed to run and control Play Station Remote Play(PSRP) using Xbox(or XInput) controllers(XC) instead of DualShock4(DS4) controllers on Windows. Although it is considered to be able to recognize and map up to 4 controllers, PSRP only accepts one controller input, so only the first connected controller can be recognized.