Monorepo for controlling MicroPython/ESP32 devices over a lightweight HTTP + WebSocket API, plus a React client library.
Purpose: provide the core infrastructure and reusable building blocks for developing ESP32-based remote-controlled actuators (e.g. remote controlled cars), including connectivity, command dispatch, device handlers, and optional camera streaming.
This repo contains three projects:
- espine-node: MicroPython device runtime (commands, handlers, WiFi, WebSocket).
- espine-cam: MicroPython camera streaming runtime (MJPEG over HTTP).
- espine-react: React hook + transport/store helpers for controlling an Espine device from React UI.
- espine-node/ — MicroPython library + tests
- espine-cam/ — MicroPython library for camera streaming
- espine-react/ — TypeScript/React library
Both device runtimes expose:
GET /health→200JSON{ "status": "ok", "environment": "esp32" | "unix" }
GET /ws(WebSocket)- Incoming text messages must be JSON of shape:
{ "type": string, "payload"?: object }
type: "heartbeat"is treated as a keepalive.type: "connection_closed"triggers a failsafe (all handlersstop()).- Incoming binary messages are passed to
CommandDispatcher.consume(...)(currently a stub). - Outgoing binary messages can be produced by handlers via
CommandDispatcher.produce(...).
- Incoming text messages must be JSON of shape:
GET /video/<frame_size>?quality=<int>- Streams
multipart/x-mixed-replaceframes (image/jpeg). frame_sizeis a key from the built-inFRAME_SIZESmapping (examples:320x240,640x480,1280x720).qualitydefaults to15.
- Streams
MicroPython-first runtime for a remote-controlled device. It follows an event-driven, stateless-actor-ish pattern:
DeviceControllerhosts HTTP routes and the/wsWebSocket.CommandDispatcherroutes commands to registered handlers.BaseHandlerdefines the handler interface (handle()andstop()).WiFiManagerconfigures STA/AP mode on-device (simulated onunix).
Code lives under:
- Library: espine-node/espinenode/
- Microdot dependency: espine-node/microdot/
- Tests: espine-node/tests/
From the repo root:
- Install/update Python deps:
cd espine-node && make upgrade - Lint:
cd espine-node && make lint - Test:
cd espine-node && make test
This monorepo does not include a device application main.py. The intended workflow is:
- Create your own firmware/app project that contains:
main.py- optionally an app folder such as
src/containing your custom handlers
- Use the template Makefile to copy
espinenode/and the vendoredmicrodot/into your project and deploy viampremote.
See the template at: espine-node/Makefile.template
A minimal sketch of a MicroPython main.py (your project) typically:
- brings up WiFi with
WiFiManager - registers handlers on a
CommandDispatcher - starts
CommandDispatcher.start()andDeviceController.start()
MicroPython camera streaming runtime (ESP32-CAM style setups).
DeviceControllerservesGET /healthandGET /video/<frame_size>- Uses the MicroPython
cameramodule (JPEG capture). - Includes a WiFi manager similar to espine-node.
Code lives under:
- Library: espine-cam/espinecam/
- Microdot dependency: espine-cam/microdot/
- Install/update Python deps:
cd espine-cam && make upgrade - Lint:
cd espine-cam && make lint
Like espine-node, the included espine-cam/Makefile.template is meant to be copied into your device project to:
- flash a camera-capable MicroPython firmware image
- deploy
espinecam/+microdot/+ your appsrc/+ yourmain.py
React library for connecting to an espine device over WebSocket.
Public API:
useDevice(url, initialState, onStream?, heartbeatInterval?, webSocket?)
It composes three internal modules:
DeviceLink: manages the WebSocket connection (JSON + binary)DeviceStore: minimal reactive store with patch updatesCommander: sends commands and emits periodic heartbeats
Code lives under: espine-react/src/
Prereqs (as declared in the project):
- Node.js
>= 22 - pnpm
>= 10
Commands:
- Install:
cd espine-react && pnpm i - Lint:
cd espine-react && pnpm lint - Test:
cd espine-react && pnpm test - Build:
cd espine-react && pnpm build
import { useDevice } from 'espine-react';
export function MyComponent() {
const { status, store, execute } = useDevice(
'ws://192.168.4.1/ws',
{ count: 0 },
(pcm) => {
// Binary frames arrive as Int16Array
console.log('stream samples', pcm.length);
},
500,
);
const state = store.getState();
return (
<div>
<div>Status: {status}</div>
<div>Count: {state.count}</div>
<button onClick={() => execute?.({ type: 'increment', payload: { amount: 1 } })}>
Increment
</button>
</div>
);
}Notes:
- JSON messages received from the device are treated as patches and merged into the store.
- Binary messages received from the device are surfaced via
onStream(Int16Array).
MIT. See LICENSE.