bub-folotoy is a Bub channel plugin for FoloToy devices.
It is based on folotoy-openclaw-plugin for the MQTT topics, credential flows, and message formats
- MQTT inbound message listening for toy speech
- Standard Bub channel dispatch for spoken replies and an explicit notification tool
- Bundled
folotoyskill undersrc/skills/folotoy - Direct official-broker auth with
toy_snandtoy_key - API-based MQTT credential exchange
- Explicit MQTT credential override for self-hosted broker setups
- Immediate transitional acknowledgment with configurable
BUB_FOLOTOY_ACK_TEXT
- Python
>=3.12
With the current Bub plugin manager:
bub install bubbuild/bub-folotoyFor local development, run uv pip install -e /path/to/bub-folotoy in the same environment as Bub.
Settings are registered under the folotoy section of Bub's config. Run bub onboard for an
interactive setup, edit ~/.bub/config.yml, or use environment variables with the
BUB_FOLOTOY_ prefix. Environment variables take precedence over the YAML file.
A minimal YAML setup for the official broker is:
enabled_channels: folotoy
folotoy:
toy_sn: your-toy-sn
toy_key: your-toy-key
mqtt_host: f.folotoy.cn
mqtt_port: 1883
ack_text: 收到,稍等一下。Start from .env.example. A minimal setup for the official broker looks like this:
BUB_FOLOTOY_TOY_SN=your-toy-sn
BUB_FOLOTOY_TOY_KEY=your-toy-key
BUB_FOLOTOY_MQTT_HOST=f.folotoy.cn
BUB_FOLOTOY_MQTT_PORT=1883
BUB_FOLOTOY_ACK_TEXT="收到,稍等一下。"
BUB_MODEL=openrouter:openai/gpt-5.4-nano
BUB_OPENROUTER_API_KEY=your-openrouter-api-key
BUB_OPENROUTER_API_BASE=https://openrouter.ai/api/v1
BUB_ENABLED_CHANNELS=folotoyIf you use a self-hosted broker, set BUB_FOLOTOY_MQTT_USERNAME and BUB_FOLOTOY_MQTT_PASSWORD.
If you use API-based MQTT credential exchange, set BUB_FOLOTOY_FLOW=api, BUB_FOLOTOY_API_URL, and BUB_FOLOTOY_API_KEY.
The MQTT host defaults to FoloToy's production broker, f.folotoy.cn. Set
BUB_FOLOTOY_MQTT_HOST (or the lower-level FOLOTOY_MQTT_HOST fallback) for testing or
self-hosted deployments.
Credential resolution order is:
- explicit MQTT username and password
- API flow when
BUB_FOLOTOY_FLOW=api - direct
toy_snandtoy_key
Defaults and resolution logic are implemented in src/bub_folotoy/folotoy.py.
The included compose.yaml deploys one FoloToy gateway for device
34cdb00c7818 with openai:gpt-5.6-sol. It reuses the host's Bub config and Codex OAuth
login without copying either credential into the image:
~/.bub/config.ymlis mounted read-only.- Only
~/.codex/auth.jsonis mounted from Codex home. It is writable because Bub refreshes expired OAuth tokens in place. - Both bind mounts use Podman's private
:ZSELinux label so a rootless container can read them without disabling SELinux isolation for the container. BUB_API_KEY,BUB_API_BASE, and fallback models are overridden withnullso an older provider configuration cannot take precedence over Codex OAuth.BUB_MAX_STEPSis capped at20to bound unexpected tool loops.- Runtime tapes and managed Bub state are kept in the
bub-folotoy-datanamed volume.
Direct broker authentication also requires the device's FoloToy key. Keep it outside the repository and add it to the mounted Bub config:
folotoy:
toy_key: your-toy-keyThen build and start the gateway:
podman compose up -d --build
podman compose ps
podman compose logs -f gatewayTo use non-default host paths, set BUB_CONFIG_FILE and/or CODEX_AUTH_FILE before running
Compose. The model and device serial number remain fixed in compose.yaml.
The MQTT topics follow folotoy-openclaw-plugin:
Inbound /openapi/folotoy/{sn}/thing/command/call
Reply /openapi/folotoy/{sn}/thing/command/callAck
Notification /openapi/folotoy/{sn}/thing/event/post
Inbound payload:
{
"msgId": 1,
"identifier": "chat_input",
"inputParams": {
"text": "hello",
"recording_id": 100
}
}Reply payload:
{
"msgId": 1,
"identifier": "chat_output",
"outParams": {
"content": "hello",
"recording_id": 100,
"order": 1,
"is_finished": false
}
}Finish frame:
{
"msgId": 1,
"identifier": "chat_output",
"outParams": {
"content": "",
"recording_id": 100,
"order": 2,
"is_finished": true
}
}Notification payload:
{
"msgId": 1,
"identifier": "send_notification",
"outParams": {
"text": "Time to drink water."
}
}At runtime the plugin creates one FoloToyMessageListener, registers the Bub channel folotoy, and injects that listener through load_state().
- inbound MQTT speech becomes a Bub
ChannelMessage - if configured, the channel sends
BUB_FOLOTOY_ACK_TEXT - the LLM's final plain-text answer is dispatched once through
FoloToyChannel.send() - proactive toy-side alerts should use
folotoy.notify - the plugin publishes one or more
chat_outputframes and a finalis_finished=trueframe
The provided skill at src/skills/folotoy/SKILL.md tells the model to use the correct tool for normal replies and one-way alerts.