Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

An open-source ESP32-C3 desktop companion robot and personal IoT avatar.

License

Notifications You must be signed in to change notification settings

SeanChangX/QBIT

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

95 Commits

Repository files navigation

繁體中文

QBIT

An open-source ESP32-C3 desktop companion robot and personal IoT avatar.

License: CC BY-NC-SA 4.0 Platform Web Platform


Assembly Video

Click the image above to watch the assembly video on YouTube.


Network
Network
Real-time device graph Poke
Poke
Send messages to devices
Flash
Flash
Browser-based firmware flasher Library
Library
Community animation repository


Hardware Requirements

Components

Component Specification Notes
MCU ESP32-C3 Super Mini (e.g. Seeed XIAO ESP32-C3) Valid GPIOs: 0-10, 20, 21
OLED Display SSD1306 128x64, I2C, address 0x3C SH1106-compatible clones also supported
Touch Sensor TTP223 capacitive touch module Digital output (HIGH when touched)
Buzzer Passive buzzer Driven via PWM (LEDC)

3D-Printed Case (STL)

Download the STL files for the QBIT enclosure from MakerWorld:

Wiring

Default pin assignments for the ESP32-C3 Super Mini. All pins can be reassigned through the web dashboard at http://qbit.local and are stored in NVS (persistent across reboots; changes require a reboot to take effect).

Function Default GPIO Direction Connection
Touch Sensor (TTP223) GPIO 1 Input TTP223 OUT -> GPIO 1
Buzzer GPIO 2 Output GPIO 2 -> Buzzer +, Buzzer - -> GND
OLED SDA GPIO 20 I2C Data SSD1306 SDA -> GPIO 20
OLED SCL GPIO 21 I2C Clock SSD1306 SCL -> GPIO 21

Power connections:

Component VCC GND
SSD1306 OLED 3.3V GND
TTP223 Touch Sensor 3.3V GND
Passive Buzzer -- GND

The I2C bus runs at 400 kHz. No external pull-up resistors are needed if the OLED module has built-in pull-ups (most breakout boards do).


Getting Started

Flash Firmware

The easiest way to flash QBIT firmware is through the browser-based flasher. No toolchain installation is required.

  1. Open the QBIT Firmware Flasher in Chrome or Edge (version 89+).
  2. Connect your QBIT board via USB.
  3. Click Connect & Flash and select the serial port.
  4. The flasher will write the bootloader, partition table, firmware, and filesystem image automatically.

Initial Wi-Fi Setup

After flashing, if Wi-Fi is not yet configured, QBIT enters Wi-Fi setup mode:

  1. The OLED shows a QR code (SSID and password) by default. Tap the touch sensor to switch to text: SSID QBIT and password (device MAC last 8 hex digits, e.g. 1A2B3C4D).
  2. Connect your phone or computer to the QBIT Wi-Fi access point using the password shown on screen.
  3. Once connected to the AP, a captive portal opens automatically (or open a browser manually to the setup page). Select your home Wi-Fi and enter its password.
  4. Credentials are saved to NVS and the device reconnects to your home Wi-Fi; you can then access the dashboard at http://qbit.local.

If Wi-Fi is lost for about 30 seconds, QBIT automatically opens the AP again so you can reconfigure.

Device Dashboard

Once connected to Wi-Fi, the QBIT hosts a local web dashboard accessible at:

http://qbit.local

From the dashboard you can:

  • View the unique device ID and set a custom display name
  • Adjust display brightness, buzzer volume, and animation playback speed
  • Upload and manage .qgif animation files
  • Configure a local MQTT broker connection for home automation (see MQTT & Home Assistant)

Web Platform

The QBIT web platform can be self-hosted and provides a central interface for monitoring and interacting with all your online QBIT devices.

If you use the official firmware, your device will automatically connect to the official server: https://qbit.labxcloud.com

If you prefer, you can deploy your own web platform and backend on your own server, and configure the firmware to connect to your custom domain.

Network

The Network page shows all currently connected QBIT devices as an interactive graph powered by vis-js/vis-network. Each node displays the device name. Devices that have been online longer are drawn closer to the central hub. The current number of online devices is displayed at the bottom.

Clicking a device node opens a poke dialog where logged-in users can:

  • Poke -- send a text message (up to 25 characters) to the device. Both the sender name and message text are rendered as 1-bit bitmaps on the web to support multi-language display (including CJK and emoji) on the monochrome OLED. If the text exceeds the screen width, it scrolls horizontally.
  • Claim -- bind a device to your account (see Device Claiming). Claimed devices show the owner's name and avatar on the graph node.
  • Unclaim -- remove your ownership of a claimed device.

Flash

The Flash page embeds the browser-based firmware flasher, allowing users to flash their QBIT directly from the web platform without visiting a separate site.

Library

The Library page is a community-driven repository of .qgif animation files.


Animation Format (.qgif)

QBIT uses a custom binary animation format (.qgif) optimized for the 128x64 monochrome OLED. The format stores 1-bit monochrome frames with per-frame delay values.

Binary layout
Offset Type Description
0 uint8 Frame count
1-2 uint16 LE Width (pixels)
3-4 uint16 LE Height (pixels)
5+ uint16 LE[] Per-frame delay (ms), one per frame
... uint8[] Frame data: 1024 bytes per frame (128x64 / 8), row-major

Converting GIFs

Use the included conversion tool to create .qgif files from standard GIF animations:

pip install Pillow
python tools/gif2qbit.py input.gif
python tools/gif2qbit.py input.gif --threshold 100 --invert --scale stretch
python tools/gif2qbit.py *.gif

Options:

Flag Description
-o / --output Output file path
--threshold Binarization threshold (0-255, default 128)
--invert Invert black/white
--scale Scaling mode: fit (default), stretch, fit_width, fit_height

Converting .qgif to C Header

To embed a .qgif animation into firmware as a PROGMEM constant (e.g. for idle or boot animations):

python tools/qgif2header.py firmware/include/sys_idle.qgif

This generates a C header file with the animation data as an AnimatedGIF struct, ready for #include in firmware source.


MQTT & Home Assistant

QBIT supports local MQTT integration with automatic Home Assistant discovery. Configure the MQTT broker connection from the device dashboard at http://qbit.local.

Once connected, the device publishes HA discovery payloads that automatically create the following entities in Home Assistant:

Entity Type Description
Status Binary Sensor Online/offline connectivity status
IP Sensor Device local IP address
Poke Button Send a poke message to the device
Last Poke Sensor Last received poke (sender, message, time as attributes)
Mute Switch Toggle buzzer mute (ON = muted)
Touch Sensor Touch gesture: single_tap, double_tap, long_press
Next Animation Button Switch to the next .qgif animation
MQTT topics (default prefix qbit)
Topic Dir Description
qbit/<id>/status Pub online / offline (retained, with LWT)
qbit/<id>/info Pub Device info JSON (id, name, ip)
qbit/<id>/command Sub Commands. JSON: {"command":"poke","sender":"...","text":"..."}
qbit/<id>/poke Pub Poke event JSON (sender, text, time)
qbit/<id>/mute/state Pub Mute state ON / OFF (retained)
qbit/<id>/mute/set Sub Set mute: ON or OFF
qbit/<id>/touch Pub Touch event JSON (type: gesture type)
qbit/<id>/animation/state Pub Currently playing animation filename (retained)
qbit/<id>/animation/next Sub Trigger switch to next animation (no payload)

Device Claiming

Logged-in users can claim a QBIT device to bind it to their account. Claimed devices display the owner's name and avatar on the Network graph.

Claiming flow:

  1. Click a device on the Network page and select "Claim this device".
  2. Enter the full 12-character device ID (printed on the device dashboard).
  3. The web sends a claim request to the device via WebSocket.
  4. The QBIT OLED displays the requester's name and prompts for a long-press confirmation.
  5. Long-press the touch button on the device to confirm, or wait 30 seconds to reject.
  6. On confirmation, the claim is stored on the server and the device shows the owner's avatar on the graph.

Unclaiming:

Click a claimed device on the Network page and select "Unclaim this device". Only the owner can unclaim.


Self-Hosting the Web Platform

Prerequisites

  • A Linux server (VPS) with Docker and Docker Compose installed
  • A domain name with DNS managed by Cloudflare (or any reverse proxy that provides TLS)
  • A Google Cloud project with OAuth 2.0 credentials
Architecture
Internet
 |
 +-- Cloudflare Tunnel / Reverse Proxy (TLS termination)
 |
 +-- frontend (Nginx, port 80)
 | |-- Serves React SPA
 | |-- Proxies /api/, /auth/, /socket.io/, /device to backend
 |
 +-- backend (Node.js, port 3001 + 3002)
 |-- REST API (devices, poke, library, claims)
 |-- Google OAuth (Passport.js)
 |-- Socket.io (real-time frontend updates)
 |-- WebSocket /device (ESP32 device connections)
 |-- Admin UI on port 3002 (sessions, users, devices, bans)
 |-- SQLite DB + files in /data (volume)

Only the frontend container exposes port 80 (mapped to host 3000) to the internet. The backend listens on 3001 (API) and 3002 (admin); restrict port 3002 to internal/VPN. Backend storage: SQLite database qbit.db, library files under /data/files/, and optional /data/secrets.json for auto-generated session and health secrets.

Environment Variables

Copy the example and fill in your values:

cd web
cp .env.example .env
Variable Description
GOOGLE_CLIENT_ID OAuth 2.0 client ID from Google Cloud Console
GOOGLE_CLIENT_SECRET OAuth 2.0 client secret
GOOGLE_CALLBACK_URL OAuth callback URL, e.g. https://yourdomain.com/auth/google/callback
COOKIE_DOMAIN Parent domain for cookies, e.g. .yourdomain.com
FRONTEND_URL Full frontend URL for CORS, e.g. https://yourdomain.com
DEVICE_API_KEY Shared secret with ESP32 firmware. Must match WS_API_KEY in firmware.
MAX_DEVICE_CONNECTIONS Max device WebSocket connections (default: 100)
ADMIN_USERNAME Admin UI login (1–64 chars). Empty = no admin login.
ADMIN_PASSWORD Admin UI password (8–128 chars).

Optional (auto-generated and stored in /data/secrets.json if not set): SESSION_SECRET, ADMIN_SESSION_SECRET, HEALTH_SECRET.

Generate secure random values (e.g. for DEVICE_API_KEY):

openssl rand -hex 32

Passwords with special characters: Use the .env file and wrap values in double quotes. Escape \ and " inside (e.g. ADMIN_PASSWORD="my\"pass").

For CI builds, set the QBIT_WS_API_KEY repository secret to the same value as DEVICE_API_KEY; it is injected into the firmware at build time.

Local Development

cd web
docker compose -f docker-compose.dev.yml up --build
Endpoint URL
Frontend http://localhost:3000
Backend API http://localhost:3001
Health http://localhost:3001/health

For Google OAuth locally, add http://localhost:3000/auth/google/callback to Authorized Redirect URIs in Google Cloud Console.

Production Deployment

cd web
docker compose up --build -d

This starts the frontend (exposed on port 3000) and backend (internal only) containers. Point your reverse proxy or Cloudflare Tunnel to the frontend service on port 3000. The frontend Nginx configuration handles proxying all API, auth, WebSocket, and Socket.io traffic to the backend internally. Only port 3000 is intended for external access; the backend is not exposed to the internet.

Verify the deployment:

docker compose ps # both containers should be running
docker compose logs backend # should show "QBIT backend listening on port 3001"

Data storage

All persistent data is in the qbit-data volume (path /data inside the backend container): SQLite database qbit.db (sessions, users, claims, bans, library metadata), uploaded library files in files/, and secrets.json for auto-generated secrets.

Logs

The backend logs to stdout (pino, JSON in production). View with docker compose logs -f backend. No log files are written.

Health check

GET /health returns server status (uptime, devices, claims, online users, library count). Use ?format=json for JSON. From the server: docker exec qbit-frontend curl -s http://backend:3001/health.

GitHub Actions CI/CD

Two workflows are included:

Build and Release (build-and-release.yml) -- triggered on version tags (v*):

  • Compiles firmware with PlatformIO
  • Builds the LittleFS filesystem image from firmware/data/
  • Injects WS_HOST and WS_API_KEY from repository secrets (QBIT_WS_HOST, QBIT_WS_API_KEY)
  • Creates a GitHub Release with firmware.bin, littlefs.bin, bootloader.bin, and partitions.bin

Deploy Flasher (deploy-gh-pages.yml) -- triggered after a successful build or on push to main:

  • Downloads the latest release artifacts
  • Generates manifest.json for esp-web-tools with partition offsets matching firmware/partitions.csv
  • Deploys the flasher tool to GitHub Pages

To set up CI/CD, add these repository secrets in GitHub (Settings > Secrets and variables > Actions > Repository secrets):

Secret Value
QBIT_WS_HOST Your backend domain (e.g. qbit.labxcloud.com)
QBIT_WS_API_KEY Same value as DEVICE_API_KEY in your .env

Firmware Build from Source

Requirements: PlatformIO CLI

cd firmware
pio run --target upload # compile and flash firmware
pio run --target uploadfs # upload LittleFS filesystem (animations, web dashboard)
pio device monitor # open serial monitor (115200 baud)

The firmware connects to the backend WebSocket server using the WS_HOST, WS_PORT, and WS_API_KEY defines in firmware/src/main.cpp. For local development, the defaults point to localhost:3001. For production builds via GitHub Actions, these values are injected from repository secrets at compile time.

Custom partition table (firmware/partitions.csv)


Tools

Tool Description
tools/gif2qbit.py Convert standard GIF animations to the .qgif format
tools/qgif2header.py Convert .qgif files to C header files for PROGMEM embedding
tools/simulate-devices.py Simulate multiple QBIT devices connecting to the backend for testing
tools/flasher/ Browser-based firmware flasher (deployed to GitHub Pages)

License

License: CC BY-NC-SA 4.0

This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).

Commercial use of this project or any derivative works is not permitted without explicit permission from the author. For commercial licensing, contact: scx@gapp.nthu.edu.tw

AltStyle によって変換されたページ (->オリジナル) /