MBXHub

by HALRAD Research

Worked example: embed a Pixelblaze controller in an MBXHub Charm

Rendered from pixelblaze-charm-worked-example.md • back to worked examples

This example uses MBXHub's documented Charm extension points to open the Pixelblaze's existing web interface inside MBXHub. The controller used for the example is http://<ipaddress>/; replace it with your own controller address. MBXHub supplies the Charm entry and wrapper; Pixelblaze supplies its controller UI and communicates with its own hardware.

The companion generation prompt asks an LLM to produce the two installation files and their documentation from llms.txt. Neither MBXHub nor PixelblazeDotNet needs to be modified or rebuilt for this approach.

Evidence, 26 September 2026: the device page and headers were fetched and inspected; local MBXHub, PixelblazeDotNet, and theMatrix source was read. The example below has not been installed, rendered in a browser, or exercised against the LEDs. It is a documented implementation example, not a report of successful end-to-end testing.

1. The brief and the chosen approach

This walkthrough shows how a small page Charm can bring an existing LAN controller into MBXHub. The companion prompt asks an LLM to create that Charm and its documentation from the public llms.txt contract.

Reusing that page means the existing pattern picker, brightness controls, pattern controls, and other device functions remain Pixelblaze's own UI. This example does not add music-linked lighting or duplicate those controls in the wrapper. It embeds the full device interface, rather than a restricted subset of its functions.

The resulting flow is:

MBXHub dashboard Charm
  -> /pages/pixelblaze.html       served by MBXHub
     -> iframe: http://<ipaddress>/
        -> ws://<ipaddress>:81/  opened by Pixelblaze's own page

The browser displaying the Charm must be able to reach the controller on its LAN. Reachability from the MBXHub server alone is insufficient. The iframe is cross-origin: the wrapper cannot read Pixelblaze's document, connection status, or controls. Pixelblaze's own status indicator is authoritative.

Why this is embedding, rather than an HTTP proxy

MBXHub's POST /api/proxy takes a JSON request describing an HTTP/HTTPS call. It is useful for device API requests, but is not a navigable reverse proxy for an entire site and does not relay WebSocket upgrades.

The inspected Pixelblaze page calls connectWebsocket(window.location.hostname) and constructs ws://<hostname>:81/. If its HTML were fetched and served under the hub's origin, that expression would target the hub instead of Pixelblaze. Its relative HTTP requests would also resolve against the new origin.

Loading the actual device URL in an iframe preserves both behaviors. A full reverse proxy would need an asset/request routing design and WebSocket relay, plus a verified treatment of that hostname-and-port construction. It is outside this example; no undocumented MBXHub endpoint is assumed.

What the device inspection established

ObservationImplication
GET / returned HTTP 200 and Content-Type: text/htmlThe controller page was reachable from the development machine.
Response headers contained Connection: close and Content-Type; neither X-Frame-Options nor Content-Security-Policy was presentNo response-header restriction on framing was observed on this response. This is not proof that every browser/host will embed it.
The page builds a WebSocket URL with its own hostname and port 81Preserve the device origin by embedding its URL directly.
Styles and scripts were present inline; the external script/link scan found the two local icon linksThere is no need to copy its large HTML bundle into MBXHub. A full runtime network audit remains unperformed.

The HTML was inspected as text. No device control command, firmware operation, pattern change, or brightness change was issued during this inspection.

2. Inputs to the LLM

Use the target MBXHub instance's /llms.txt as the primary integration contract. The published reference is mbxhub.com/llms.txt. A live instance's reference is preferable when its version differs from the published site.

Read these sections before generating the Charm:

For controller context, these references informed the example:

ReferenceWhat it provides
PixelblazeDotNetThe .NET controller library. The inspected local checkout contained PixelblazeClient.cs, PixelblazeNetworkHelper.cs, PixelblazePatterns.cs, and PixelblazeSettings.cs.
MBXHub llms.txtThe public API and page-Charm contract; prefer your running instance's copy when versions differ.
Charms SDKAdditional Charm integration documentation.

The initial research also read a local theMatrix Electromage plugin as a usage example. That checkout is not required to follow this walkthrough. The library README described its existing status as "Wet Paint" when inspected; the example does not claim to change the library's status.

These controller references informed the design; they are not new runtime dependencies. In particular, a browser cannot directly consume this .NET 8 library. No local bridge, .NET process, or protocol port is needed for embedding.

An instructive source discrepancy: theMatrix's pattern-selection comment says setActiveProgram, while the inspected library actually serializes activeProgramId. The library also handles binary pattern-list responses (message type 7), brightness in the range 0–1, and save: false by default for brightness/control writes. This is why a future custom controller should follow implementation and observed responses rather than copy a comment. The embedded page already owns this protocol handling.

3. Installation files

The following are the complete contents of the two example files to generate. Copy each code block into the indicated file. This walkthrough documents the files; it does not install them into an MBXHub instance.

charms/pixelblaze.json

Use the existing expand[] button shape, with a hub-hosted page as the target. The namespaced example ID avoids claiming another publisher's identity.

{
  "id": "example.pixelblaze",
  "kind": "page",
  "label": "Pixelblaze",
  "expand": [
    {
      "icon": "<svg viewBox='0 0 16 16' width='16' height='16' fill='currentColor'><circle cx='4' cy='4' r='2'/><circle cx='12' cy='4' r='2'/><circle cx='4' cy='12' r='2'/><circle cx='12' cy='12' r='2'/></svg>",
      "label": "Pixelblaze",
      "action": "webapp /pages/pixelblaze.html",
      "display": "both"
    }
  ]
}

This is a locally placed page manifest, like the documented drop-in examples. It does not fabricate a registration block or claim a proxy:lan grant: the wrapper does not call the hub's device proxy. Direct device access is a browser-to-device connection, not an MBXHub-mediated permission grant. Honor the installed hub's registration policy if it requires additional setup; any required approval belongs in its existing console workflow.

pages/pixelblaze.html

This wrapper supplies an accessible frame title, a reload button, and a link to open the device separately, in a header you can collapse to give the device page the room. The wrapper adopts MBXHub's theme; the embedded device UI retains Pixelblaze's own theme.

<!doctype html>
<html lang="en" data-theme="__DATA_THEME__">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Pixelblaze</title>
  <link rel="stylesheet" href="/pages/components/shared.css">
  <style id="theme-inline">__THEME_CSS__</style>
  <style>
    * { box-sizing: border-box; }
    html, body { margin: 0; height: 100%; }
    body { display: flex; flex-direction: column; font-family: system-ui, sans-serif; }
    header { padding: .75rem; border-bottom: 1px solid currentColor; }
    /* The controls collapse so the device page can have the room. The title is the
       summary, so the collapsed header is one line and still says what this is. */
    summary { cursor: pointer; }
    h1 { display: inline; margin: 0; font-size: 1.1rem; }
    details[open] > summary { margin-bottom: .5rem; }
    nav { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; }
    button, a { font: inherit; }
    button { padding: .4rem .65rem; cursor: pointer; }
    a { color: inherit; }
    #status { margin: .5rem 0 0; font-size: .9rem; }
    iframe { width: 100%; flex: 1; min-height: 0; border: 0; }
  </style>
  <script src="/pages/components/shared.js" defer></script>
</head>
<body>
  <header>
    <details id="controls" open>
    <summary><h1>Pixelblaze</h1></summary>
    <nav aria-label="Controller page">
      <button id="reload" type="button">Reload device page</button>
      <a id="open" href="http://<ipaddress>/" target="_blank"
         rel="noopener noreferrer">Open device separately</a>
    </nav>
    <p id="status" role="status">Opening the device page…</p>
    </details>
  </header>
  <iframe id="device" title="Pixelblaze LED controller"
          referrerpolicy="no-referrer"></iframe>
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      const deviceUrl = 'http://<ipaddress>/';
      const frame = document.getElementById('device');
      const status = document.getElementById('status');
      const reload = document.getElementById('reload');
      document.getElementById('open').href = deviceUrl;

      // Open or collapsed, remembered per browser. Storage that throws (private
      // windows, blocked site data) just leaves the controls open.
      const controls = document.getElementById('controls');
      try {
        if (localStorage.getItem('pixelblaze.controls') === 'closed') controls.open = false;
      } catch (e) { }
      controls.addEventListener('toggle', function () {
        try { localStorage.setItem('pixelblaze.controls', controls.open ? 'open' : 'closed'); } catch (e) { }
      });

      function log(level, message) {
        if (window.MBXShared && window.MBXShared.clientLog) {
          window.MBXShared.clientLog(level, '[pixelblaze] ' + message);
        }
      }

      function loadDevice() {
        if (location.protocol !== 'http:') {
          status.textContent = 'This HTTP device needs an HTTP host page. Use “Open device separately”.';
          reload.disabled = true;
          log('warn', 'Inline load skipped: host page is not HTTP.');
          return;
        }
        status.textContent = 'Opening Pixelblaze. If it stays blank, open the device separately.';
        log('info', 'Device frame navigation requested.');
        frame.src = deviceUrl;
      }

      frame.addEventListener('load', function () {
        if (!frame.hasAttribute('src')) return;
        // A cross-origin iframe load event is not a connectivity check.
        status.textContent = 'Use Pixelblaze’s own connection status below. If the page is blank, open the device separately.';
        log('debug', 'Frame load event received; controller connection is not observable here.');
      });
      reload.addEventListener('click', loadDevice);
      loadDevice();
    });
  </script>
</body>
</html>

The example uses a fixed device address deliberately. Changing devices means editing the deviceUrl value and the fallback link's href. An address picker, device discovery, and persistence are separate enhancements. The wrapper does not attempt cross-origin DOM access, inject code into the controller page, or add an iframe-cmd action that Pixelblaze does not implement.

4. Install and use the generated files

  1. Identify the active MBXHub instance's data directory. Use its existing pages/ and charms/ folders; do not assume a path from a different install.
  2. Place the manifest at charms/pixelblaze.json and the wrapper at pages/pixelblaze.html. Preserve any existing files with those names before replacing them. These are data-directory files, not embedded DLL resources.
  3. Open the hub's /pages/pixelblaze.html over HTTP to check the wrapper directly. Then reload the dashboard and open its Pixelblaze Charm. Follow the installed version's documented Charm refresh/registration workflow if it does not appear.
  4. The first example action uses display: "both": normal activation opens inline; the documented Shift-click action opens the wrapper separately. The wrapper's Open device separately link instead opens the actual controller URL.
  5. Confirm connection using the status inside Pixelblaze's UI. Select patterns, adjust brightness, and use pattern controls there as you would on its own page.

No MBXHub build, process restart, deployment script change, or library installation is part of these installation steps. When using an AI agent, review the generated files before authorizing installation or hardware changes.

To remove this example, remove only its two installed files and refresh the dashboard. Removing the wrapper is not a command to stop the LEDs or restore a previous pattern. Avoid leaving multiple controller tabs open unnecessarily.

5. Verification checklist and limits

Run these checks on the generated files before describing the integration as working. Record actual results rather than copying this checklist as a pass.

CheckExpected observation
Parse the manifest and inspect its actionValid JSON; action targets /pages/pixelblaze.html; fields match the target hub's llms.txt.
Inspect served wrapper bytesThe running hub serves this wrapper, rather than an older data-directory override. Theme tokens have been resolved.
Firefox first: wrapper directly, then dashboard inlineDevice page is visible at a usable size; keyboard focus can reach wrapper actions and device controls.
Chrome, Edge, and Safari where availableSame core navigation and embedding behavior; record any browser/OS combination not tested.
Controller connection inside the framePixelblaze reports its connection; browser network tools show the expected device port 81. A wrapper load event is not sufficient.
Hardware operation, with authorizationRecord the starting state, select a known pattern and modest brightness change, confirm LEDs respond, then restore the starting state.
Narrow viewport and scrollingWrapper actions remain reachable; the device page remains usable within its own layout.
Unreachable test address in a separate test copyThe wrapper does not report "Connected"; the separate-device link remains available. Do not stop the user's controller for this check.
HTTPS-hosted hubInline loading is skipped with explanatory text; the direct-device link remains usable. No browser security bypass is suggested.
LAN without internet accessWrapper and core controller operations still function. Optional manufacturer links/features are not an offline guarantee.

The device's current response headers make direct embedding a plausible fit, but actual browser framing and WebSocket behavior have not yet been checked. Browser local-network policies, a host content policy, firmware changes, or an HTTPS host page may affect it. When embedding fails, check the browser's actual message and the controller response before selecting a different architecture.

The full Pixelblaze UI remains independently owned: MBXHub cannot theme its inner document, report its live state, or safely automate its controls through the cross-origin frame. Music-linked lighting would need a separate, deliberate integration using the device protocol or a local helper; it is not supplied by putting the controller page inside a Charm.

6. How to use and evaluate the generation prompt

Copy the companion prompt into an LLM session with the target hub's /llms.txt available. Replace the hub URL placeholder with the actual host and port. Keep the device URL as http://<ipaddress>/ for this example, or replace it explicitly. Provide local source access only if you have it; the public prompt does not require theMatrix or MBXHub source access.

The prompt separates three facts: MBXHub defines the extension contract, the device defines its UI and transport, and the generated wrapper supplies only the embedding/navigation. It asks for installable files plus usage and implementation documentation, and requires a verification record that distinguishes static checks from browser and hardware checks. This guards against a convincing page being presented as a tested controller integration.

Verification record for this document