{"id":44085707,"url":"https://github.com/seanchangx/qbit","last_synced_at":"2026-03-05T00:09:07.951Z","repository":{"id":337100972,"uuid":"1151893724","full_name":"SeanChangX/QBIT","owner":"SeanChangX","description":"An open-source ESP32-C3 desktop companion robot and personal IoT avatar.","archived":false,"fork":false,"pushed_at":"2026-02-18T12:22:06.000Z","size":3615,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-18T14:53:48.519Z","etag":null,"topics":["companion-robot","desktop-robot","esp32","esp32-c3","home-assistant","iot","qbit","robotics"],"latest_commit_sha":null,"homepage":"https://qbit.labxcloud.com","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SeanChangX.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-07T03:32:49.000Z","updated_at":"2026-02-18T12:22:09.000Z","dependencies_parsed_at":"2026-02-22T18:01:11.578Z","dependency_job_id":null,"html_url":"https://github.com/SeanChangX/QBIT","commit_stats":null,"previous_names":["seanchangx/qbit"],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/SeanChangX/QBIT","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeanChangX%2FQBIT","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeanChangX%2FQBIT/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeanChangX%2FQBIT/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeanChangX%2FQBIT/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SeanChangX","download_url":"https://codeload.github.com/SeanChangX/QBIT/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeanChangX%2FQBIT/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29721046,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-22T15:10:41.462Z","status":"ssl_error","status_checked_at":"2026-02-22T15:10:04.636Z","response_time":110,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["companion-robot","desktop-robot","esp32","esp32-c3","home-assistant","iot","qbit","robotics"],"created_at":"2026-02-08T10:03:49.981Z","updated_at":"2026-02-22T18:01:28.388Z","avatar_url":"https://github.com/SeanChangX.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"right\"\u003e\u003ca href=\"README.zh-TW.md\"\u003e繁體中文\u003c/a\u003e\u003c/p\u003e\n\n\u003cdiv align=\"center\"\u003e\n\n# QBIT\n\n**An open-source ESP32-C3 desktop companion robot and personal IoT avatar.**\n\n[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC_BY--NC--SA_4.0-blue.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)\n[![Platform](https://img.shields.io/badge/Platform-ESP32--C3-green.svg)](#hardware-requirements)\n[![Web Platform](https://img.shields.io/badge/Web-qbit.labxcloud.com-purple.svg)](https://qbit.labxcloud.com)\n\n\u003cbr\u003e\n\n[![Assembly Video](https://img.youtube.com/vi/pUKB8I10Yfk/maxresdefault.jpg)](https://youtu.be/pUKB8I10Yfk)\n\n*Click the image above to watch the assembly video on YouTube.*\n\n\u003c/div\u003e\n\n\u003cbr\u003e\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd width=\"50%\" align=\"center\"\u003e\n\u003cimg src=\"docs/images/Network.png\" alt=\"Network\" width=\"100%\"\u003e\n\u003cbr\u003e\u003cstrong\u003eNetwork\u003c/strong\u003e\u003cbr\u003eReal-time device graph\n\u003c/td\u003e\n\u003ctd width=\"50%\" align=\"center\"\u003e\n\u003cimg src=\"docs/images/Poke.png\" alt=\"Poke\" width=\"100%\"\u003e\n\u003cbr\u003e\u003cstrong\u003ePoke\u003c/strong\u003e\u003cbr\u003eSend messages to devices\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd width=\"50%\" align=\"center\"\u003e\n\u003cimg src=\"docs/images/Flash.png\" alt=\"Flash\" width=\"100%\"\u003e\n\u003cbr\u003e\u003cstrong\u003eFlash\u003c/strong\u003e\u003cbr\u003eBrowser-based firmware flasher\n\u003c/td\u003e\n\u003ctd width=\"50%\" align=\"center\"\u003e\n\u003cimg src=\"docs/images/Library.png\" alt=\"Library\" width=\"100%\"\u003e\n\u003cbr\u003e\u003cstrong\u003eLibrary\u003c/strong\u003e\u003cbr\u003eCommunity animation repository\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n\u003cbr\u003e\n\n\u003cdiv align=\"center\"\u003e\n\n[**Getting Started**](#getting-started) \u0026#8226;\n[**Hardware**](#hardware-requirements) \u0026#8226;\n[**Web Platform**](#web-platform) \u0026#8226;\n[**MQTT**](#mqtt--home-assistant) \u0026#8226;\n[**Self-Hosting**](#self-hosting-the-web-platform) \u0026#8226;\n[**Build from Source**](#firmware-build-from-source)\n\n\u003c/div\u003e\n\n---\n\n## Hardware Requirements\n\n### Components\n\n| Component | Specification | Notes |\n|---|---|---|\n| MCU | ESP32-C3 Super Mini (e.g. Seeed XIAO ESP32-C3) | Valid GPIOs: 0-10, 20, 21 |\n| OLED Display | SSD1306 128x64, I2C, address 0x3C | SH1106-compatible clones also supported |\n| Touch Sensor | TTP223 capacitive touch module | Digital output (HIGH when touched) |\n| Buzzer | Passive buzzer | Driven via PWM (LEDC) |\n\n### 3D-Printed Case (STL)\n\nDownload the STL files for the QBIT enclosure from MakerWorld:\n\n- [MakerWorld: QBIT - Your IoT Desk Robot (ESP32-C3)](https://makerworld.com/en/models/2400803-qbit-your-iot-desk-robot-esp32-c3#profileId-2631417)\n\n### Wiring\n\nDefault 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).\n\n| Function | Default GPIO | Direction | Connection |\n|---|---|---|---|\n| Touch Sensor (TTP223) | GPIO 1 | Input | TTP223 OUT -\u003e GPIO 1 |\n| Buzzer | GPIO 2 | Output | GPIO 2 -\u003e Buzzer +, Buzzer - -\u003e GND |\n| OLED SDA | GPIO 20 | I2C Data | SSD1306 SDA -\u003e GPIO 20 |\n| OLED SCL | GPIO 21 | I2C Clock | SSD1306 SCL -\u003e GPIO 21 |\n\nPower connections:\n\n| Component | VCC | GND |\n|---|---|---|\n| SSD1306 OLED | 3.3V | GND |\n| TTP223 Touch Sensor | 3.3V | GND |\n| Passive Buzzer | -- | GND |\n\nThe 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).\n\n---\n\n## Getting Started\n\n### Flash Firmware\n\nThe easiest way to flash QBIT firmware is through the browser-based flasher. No toolchain installation is required.\n\n1. Open the [QBIT Firmware Flasher](https://seanchangx.github.io/QBIT/) in Chrome or Edge (version 89+).\n2. Connect your QBIT board via USB.\n3. Click **Connect \u0026 Flash** and select the serial port.\n4. The flasher will write the bootloader, partition table, firmware, and filesystem image automatically.\n\n### Initial Wi-Fi Setup\n\nAfter flashing, if Wi-Fi is not yet configured, QBIT enters Wi-Fi setup mode:\n\n1. 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`).\n2. Connect your phone or computer to the **QBIT** Wi-Fi access point using the password shown on screen.\n3. 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.\n4. Credentials are saved to NVS and the device reconnects to your home Wi-Fi; you can then access the dashboard at `http://qbit.local`.\n\nIf Wi-Fi is lost for about 30 seconds, QBIT automatically opens the AP again so you can reconfigure.\n\n### Device Dashboard\n\nOnce connected to Wi-Fi, the QBIT hosts a local web dashboard accessible at:\n\n```\nhttp://qbit.local\n```\n\nFrom the dashboard you can:\n\n- View the unique device ID and set a custom display name\n- Adjust display brightness, buzzer volume, and animation playback speed\n- Upload and manage .qgif animation files\n- Configure a local MQTT broker connection for home automation (see [MQTT \u0026 Home Assistant](#mqtt--home-assistant))\n\n---\n\n## Web Platform\n\nThe QBIT web platform can be self-hosted and provides a central interface for monitoring and interacting with all your online QBIT devices.\n\nIf you use the official firmware, your device will automatically connect to the official server:\n[https://qbit.labxcloud.com](https://qbit.labxcloud.com)\n\nIf 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.\n\n### Network\n\nThe Network page shows all currently connected QBIT devices as an interactive graph powered by [vis-js/vis-network](https://github.com/visjs/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.\n\nClicking a device node opens a poke dialog where logged-in users can:\n\n- **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.\n- **Claim** -- bind a device to your account (see [Device Claiming](#device-claiming)). Claimed devices show the owner's name and avatar on the graph node.\n- **Unclaim** -- remove your ownership of a claimed device.\n\n### Flash\n\nThe Flash page embeds the browser-based firmware flasher, allowing users to flash their QBIT directly from the web platform without visiting a separate site.\n\n### Library\n\nThe Library page is a community-driven repository of .qgif animation files.\n\n---\n\n## Animation Format (.qgif)\n\nQBIT 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.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eBinary layout\u003c/strong\u003e\u003c/summary\u003e\n\u003cbr\u003e\n\n| Offset | Type | Description |\n|---|---|---|\n| 0 | uint8 | Frame count |\n| 1-2 | uint16 LE | Width (pixels) |\n| 3-4 | uint16 LE | Height (pixels) |\n| 5+ | uint16 LE[] | Per-frame delay (ms), one per frame |\n| ... | uint8[] | Frame data: 1024 bytes per frame (128x64 / 8), row-major |\n\n\u003c/details\u003e\n\n### Converting GIFs\n\nUse the included conversion tool to create .qgif files from standard GIF animations:\n\n```bash\npip install Pillow\npython tools/gif2qbit.py input.gif\npython tools/gif2qbit.py input.gif --threshold 100 --invert --scale stretch\npython tools/gif2qbit.py *.gif\n```\n\nOptions:\n\n| Flag | Description |\n|---|---|\n| `-o` / `--output` | Output file path |\n| `--threshold` | Binarization threshold (0-255, default 128) |\n| `--invert` | Invert black/white |\n| `--scale` | Scaling mode: `fit` (default), `stretch`, `fit_width`, `fit_height` |\n\n### Converting .qgif to C Header\n\nTo embed a .qgif animation into firmware as a PROGMEM constant (e.g. for idle or boot animations):\n\n```bash\npython tools/qgif2header.py firmware/include/sys_idle.qgif\n```\n\nThis generates a C header file with the animation data as an `AnimatedGIF` struct, ready for `#include` in firmware source.\n\n---\n\n## MQTT \u0026 Home Assistant\n\nQBIT supports local MQTT integration with automatic Home Assistant discovery. Configure the MQTT broker connection from the device dashboard at `http://qbit.local`.\n\nOnce connected, the device publishes HA discovery payloads that automatically create the following entities in Home Assistant:\n\n| Entity | Type | Description |\n|---|---|---|\n| Status | Binary Sensor | Online/offline connectivity status |\n| IP | Sensor | Device local IP address |\n| Poke | Button | Send a poke message to the device |\n| Last Poke | Sensor | Last received poke (sender, message, time as attributes) |\n| Mute | Switch | Toggle buzzer mute (ON = muted) |\n| Touch | Sensor | Touch gesture: `single_tap`, `double_tap`, `long_press` |\n| Next Animation | Button | Switch to the next .qgif animation |\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eMQTT topics\u003c/strong\u003e (default prefix \u003ccode\u003eqbit\u003c/code\u003e)\u003c/summary\u003e\n\u003cbr\u003e\n\n| Topic | Dir | Description |\n|---|---|---|\n| `qbit/\u003cid\u003e/status` | Pub | `online` / `offline` (retained, with LWT) |\n| `qbit/\u003cid\u003e/info` | Pub | Device info JSON (`id`, `name`, `ip`) |\n| `qbit/\u003cid\u003e/command` | Sub | Commands. JSON: `{\"command\":\"poke\",\"sender\":\"...\",\"text\":\"...\"}` |\n| `qbit/\u003cid\u003e/poke` | Pub | Poke event JSON (`sender`, `text`, `time`) |\n| `qbit/\u003cid\u003e/mute/state` | Pub | Mute state `ON` / `OFF` (retained) |\n| `qbit/\u003cid\u003e/mute/set` | Sub | Set mute: `ON` or `OFF` |\n| `qbit/\u003cid\u003e/touch` | Pub | Touch event JSON (`type`: gesture type) |\n| `qbit/\u003cid\u003e/animation/state` | Pub | Currently playing animation filename (retained) |\n| `qbit/\u003cid\u003e/animation/next` | Sub | Trigger switch to next animation (no payload) |\n\n\u003c/details\u003e\n\n---\n\n## Device Claiming\n\nLogged-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.\n\n**Claiming flow:**\n\n1. Click a device on the Network page and select \"Claim this device\".\n2. Enter the full 12-character device ID (printed on the device dashboard).\n3. The web sends a claim request to the device via WebSocket.\n4. The QBIT OLED displays the requester's name and prompts for a long-press confirmation.\n5. Long-press the touch button on the device to confirm, or wait 30 seconds to reject.\n6. On confirmation, the claim is stored on the server and the device shows the owner's avatar on the graph.\n\n**Unclaiming:**\n\nClick a claimed device on the Network page and select \"Unclaim this device\". Only the owner can unclaim.\n\n---\n\n## Self-Hosting the Web Platform\n\n### Prerequisites\n\n- A Linux server (VPS) with Docker and Docker Compose installed\n- A domain name with DNS managed by Cloudflare (or any reverse proxy that provides TLS)\n- A Google Cloud project with OAuth 2.0 credentials\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eArchitecture\u003c/strong\u003e\u003c/summary\u003e\n\u003cbr\u003e\n\n```\nInternet\n  |\n  +-- Cloudflare Tunnel / Reverse Proxy (TLS termination)\n        |\n        +-- frontend (Nginx, port 80)\n        |     |-- Serves React SPA\n        |     |-- Proxies /api/, /auth/, /socket.io/, /device to backend\n        |\n        +-- backend (Node.js, port 3001 + 3002)\n              |-- REST API (devices, poke, library, claims)\n              |-- Google OAuth (Passport.js)\n              |-- Socket.io (real-time frontend updates)\n              |-- WebSocket /device (ESP32 device connections)\n              |-- Admin UI on port 3002 (sessions, users, devices, bans)\n              |-- SQLite DB + files in /data (volume)\n```\n\nOnly 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.\n\n\u003c/details\u003e\n\n### Environment Variables\n\nCopy the example and fill in your values:\n\n```bash\ncd web\ncp .env.example .env\n```\n\n| Variable | Description |\n|---|---|\n| `GOOGLE_CLIENT_ID` | OAuth 2.0 client ID from Google Cloud Console |\n| `GOOGLE_CLIENT_SECRET` | OAuth 2.0 client secret |\n| `GOOGLE_CALLBACK_URL` | OAuth callback URL, e.g. `https://yourdomain.com/auth/google/callback` |\n| `COOKIE_DOMAIN` | Parent domain for cookies, e.g. `.yourdomain.com` |\n| `FRONTEND_URL` | Full frontend URL for CORS, e.g. `https://yourdomain.com` |\n| `DEVICE_API_KEY` | Shared secret with ESP32 firmware. Must match `WS_API_KEY` in firmware. |\n| `MAX_DEVICE_CONNECTIONS` | Max device WebSocket connections (default: 100) |\n| `ADMIN_USERNAME` | Admin UI login (1–64 chars). Empty = no admin login. |\n| `ADMIN_PASSWORD` | Admin UI password (8–128 chars). |\n\nOptional (auto-generated and stored in `/data/secrets.json` if not set): `SESSION_SECRET`, `ADMIN_SESSION_SECRET`, `HEALTH_SECRET`.\n\nGenerate secure random values (e.g. for `DEVICE_API_KEY`):\n\n```bash\nopenssl rand -hex 32\n```\n\n**Passwords with special characters:** Use the `.env` file and wrap values in double quotes. Escape `\\` and `\"` inside (e.g. `ADMIN_PASSWORD=\"my\\\"pass\"`).\n\nFor 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.\n\n### Local Development\n\n```bash\ncd web\ndocker compose -f docker-compose.dev.yml up --build\n```\n\n| Endpoint | URL |\n|---|---|\n| Frontend | http://localhost:3000 |\n| Backend API | http://localhost:3001 |\n| Health | http://localhost:3001/health |\n\nFor Google OAuth locally, add `http://localhost:3000/auth/google/callback` to Authorized Redirect URIs in Google Cloud Console.\n\n### Production Deployment\n\n```bash\ncd web\ndocker compose up --build -d\n```\n\nThis 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.\n\nVerify the deployment:\n\n```bash\ndocker compose ps             # both containers should be running\ndocker compose logs backend   # should show \"QBIT backend listening on port 3001\"\n```\n\n### Data storage\n\nAll 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.\n### Logs\n\nThe backend logs to stdout (pino, JSON in production). View with `docker compose logs -f backend`. No log files are written.\n\n### Health check\n\n`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`.\n\n### GitHub Actions CI/CD\n\nTwo workflows are included:\n\n**Build and Release** (`build-and-release.yml`) -- triggered on version tags (`v*`):\n- Compiles firmware with PlatformIO\n- Builds the LittleFS filesystem image from `firmware/data/`\n- Injects `WS_HOST` and `WS_API_KEY` from repository secrets (`QBIT_WS_HOST`, `QBIT_WS_API_KEY`)\n- Creates a GitHub Release with `firmware.bin`, `littlefs.bin`, `bootloader.bin`, and `partitions.bin`\n\n**Deploy Flasher** (`deploy-gh-pages.yml`) -- triggered after a successful build or on push to main:\n- Downloads the latest release artifacts\n- Generates `manifest.json` for esp-web-tools with partition offsets matching `firmware/partitions.csv`\n- Deploys the flasher tool to GitHub Pages\n\nTo set up CI/CD, add these repository secrets in GitHub (Settings \u003e Secrets and variables \u003e Actions \u003e Repository secrets):\n\n| Secret | Value |\n|---|---|\n| `QBIT_WS_HOST` | Your backend domain (e.g. `qbit.labxcloud.com`) |\n| `QBIT_WS_API_KEY` | Same value as `DEVICE_API_KEY` in your `.env` |\n\n---\n\n## Firmware Build from Source\n\nRequirements: [PlatformIO CLI](https://docs.platformio.org/en/latest/core/installation/index.html)\n\n```bash\ncd firmware\npio run --target upload         # compile and flash firmware\npio run --target uploadfs       # upload LittleFS filesystem (animations, web dashboard)\npio device monitor              # open serial monitor (115200 baud)\n```\n\nThe 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.\n\nCustom partition table ([`firmware/partitions.csv`](firmware/partitions.csv))\n\n---\n\n## Tools\n\n| Tool | Description |\n|---|---|\n| `tools/gif2qbit.py` | Convert standard GIF animations to the .qgif format |\n| `tools/qgif2header.py` | Convert .qgif files to C header files for PROGMEM embedding |\n| `tools/simulate-devices.py` | Simulate multiple QBIT devices connecting to the backend for testing |\n| `tools/flasher/` | Browser-based firmware flasher (deployed to GitHub Pages) |\n\n---\n\n## License\n\n[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)\n\nThis project is licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/).\n\nCommercial 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\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseanchangx%2Fqbit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseanchangx%2Fqbit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseanchangx%2Fqbit/lists"}