Firmware 3.10.4 serves the local WebSocket endpoint at ws://HAT_IP:81/. It streams state and accepts chamber target commands. It does not accept arbitrary HTTP API actions over the socket.
Use HTTP controls for bed and hotend scaling, the fan, and printer commands. These details describe the current Bambu firmware; they do not establish an API for a separate Prusa firmware.
For placeholder address 192.0.2.42, connect to:
ws://192.0.2.42:81/
The upgrade request must have the normal host and an origin matching the HAT's HTTP dashboard:
Host: 192.0.2.42:81
Origin: http://192.0.2.42
Origin: http://192.0.2.42:80 is also accepted. Origin: http://192.0.2.42:81, an omitted origin, a different hostname, and an https:// origin are rejected. Use one consistent address spelling: connecting by IP while sending a .local origin is a mismatch. The host must itself be an address the HAT recognizes.
When optional access protection is enabled, the handshake requires a valid dashboard session cookie or, if Developer API Access is enabled, X-Tyson-Access-Code. The developer code is separate from dashboard login. There is no WebSocket subprotocol requirement. The HTTP mutation marker X-Tyson-Request is not used for WebSocket messages. These are local unencrypted HTTP/WebSocket services; an external HTTPS website cannot directly treat them as a cross-origin backend. Code rotation, session logout/expiry, or enabling protection revoke affected existing sockets; authenticate again before reconnecting.
Native clients should set the handshake origin through their WebSocket library. Browser JavaScript cannot choose an arbitrary Origin header: it must run from the matching HAT dashboard origin. Hosting the same example in the Wiki.js page, opening it from disk, or running it on localhost will not make the request acceptable.
Send the literal text message below after opening:
get_state
The reply is a full JSON state snapshot, using the same fields as GET /api/state. No subscription message is required. The HAT also schedules a broadcast approximately every 500 milliseconds while connected. This is not a guaranteed sampling interval: delayed clients may miss superseded periodic snapshots.
A small browser example, intended to run in the HAT dashboard's origin, reads state without changing targets:
const socket = new WebSocket(`ws://${location.hostname}:81/`);
socket.addEventListener('open', () => socket.send('get_state'));
socket.addEventListener('message', event => {
const message = JSON.parse(event.data);
if (message.error) {
console.error(message.error, message.ack_id);
return;
}
console.log({
boot: message.boot_id,
sequence: message.state_seq,
target: message.chamber_setpoint,
temperature: message.chamber_temp_valid
? message.chamber_temperature : null
});
});
// Close the extra connection when finished: socket.close();
The server allows up to four WebSocket-server sockets. Reuse one connection per application and close unused connections.
Send one complete text frame containing JSON:
{"id":1,"method":"set_setpoint","value":55}
| Field | Requirement |
|---|---|
id |
Integer from 1 through 2147483647; use a new value for each outstanding command. |
method |
Exact string set_setpoint. |
value |
Integer from 0 through 108, in °C. 0 requests chamber heating off. |
The controller clamps accepted values to its currently configured chamber mode:
max_temp_mode |
Dashboard name | max_setpoint |
|---|---|---|
0 |
Standard | 60 |
1 |
Advanced | 90 |
2 |
Expert | 108 |
For example, a syntactically valid request for 90 while Standard mode is selected applies 60°C. A value above 108, a negative value, a fraction, or a string such as "55" is invalid. Read max_setpoint before composing the request and use the acknowledged chamber_setpoint as the result, rather than assuming the requested value was applied.
To request off, send a new command ID with zero:
{"id":2,"method":"set_setpoint","value":0}
Setting a target does not establish that heating is active. Sensor validity, fan readiness, power configuration, and firmware operating conditions still control the output. Setting zero may leave the chamber fan running for cooling. Read heater_output, chamber_relay_output, chamber_temp_valid, and fan state to describe what the controller is doing.
A successful command receives a full state snapshot with ack_id equal to the sent id. This abbreviated example shows only the relevant fields; the real response has many more:
{
"boot_id": "example-boot",
"state_seq": 42,
"ack_id": 1,
"chamber_setpoint": 55,
"max_setpoint": 60,
"persistence_pending": true,
"persistence_error": false
}
get_state replies and periodic broadcasts normally have no ack_id. Match the ID explicitly; an unrelated broadcast with the requested temperature is not the command's acknowledgment. IDs correlate responses only: the firmware does not provide deduplication or exactly-once execution for repeated IDs.
The acknowledgment confirms that the controller applied its in-memory target. If persist_setpoint is enabled, saving to flash happens afterward. Wait for persistence_pending: false and inspect persistence_error when your integration needs to report persistence. If target persistence is disabled, the target resets to zero after reboot.
Invalid or currently blocked commands receive an error object, for example:
{"error":"Invalid setpoint command","ack_id":1}
An invalid/missing ID is reported as ack_id: 0. Other errors include Unknown command, Factory reset in progress, and Firmware update or startup verification in progress. These are socket messages, not HTTP status codes. The original connection upgrade can fail with HTTP 403 if its address or origin is rejected.
Use the pair boot_id and state_seq when merging WebSocket snapshots with HTTP responses:
boot_id must be a string.boot_id changes, clear the previous sequence and pending-operation assumptions. Read the restarted controller's actual state.state_seq is newer than the most recently applied snapshot. Sequence values are generated for snapshots; they are not elapsed time or command IDs.ack_id separately, even if its snapshot is older than another already-received snapshot. This prevents an older HTTP response from rewinding live state without losing acknowledgment tracking.Treat chamber_temperature as usable only when chamber_temp_valid is true. A numeric reading can remain in the snapshot after it becomes invalid. Bed and nozzle readings have their own validity fields and can be null.
The live state stream is the dashboard's application heartbeat. There is no JSON ping command. get_state is a supported way to request a fresh snapshot. Standard WebSocket protocol ping/pong handling is separate from application text messages.
The shipped dashboard uses these client-side timings:
| Condition | Dashboard behavior |
|---|---|
No valid state for more than 6 seconds |
Treats the connection as lost; checked once per second. |
No matching target acknowledgment within 4 seconds |
Disconnects and reconnects to read the actual target. |
| Disconnected | Schedules another connection attempt after 1 second. |
These are reference client behaviors, not a server promise that commands expire after four seconds. A timeout can occur after the command was applied. Do not automatically replay a heating command after reconnecting. Obtain a fresh snapshot, show the actual target, and let the caller decide whether a new command is appropriate. A connection failure does not automatically zero a previously accepted chamber target.
Keep target edits serialized or explicitly track each outstanding ID. The dashboard queues one newer edit behind a pending request; a user off request supersedes that queue and is sent immediately with a new ID. WebSocket delivery order puts that off request after earlier commands already sent on the same connection.
The server preserves ordered command replies while dropping obsolete periodic snapshots for a slow peer. If its output queue overflows or cannot drain within its send timeout, it closes the connection. Keep reading messages even when your application is not displaying every update.
Application messages must be non-empty, final text frames no longer than 256 bytes. Unsupported frame forms can close the connection instead of producing a JSON error. Do not fragment the small command payload.
The older text form chamber_setpoint:55 is still accepted, with the same target range and mode clamp, but has no correlation ID. New clients should use the JSON form. The literal get_state is still the supported state request; there is no corresponding JSON method: "get_state" command.
Source basis: main/main.cpp (ws_handler, WebSocket handshake, broadcast and output queue handling), main/web_request_policy.h, main/controller.h, and main/web/app.js. See Developer reference for the overall API scope.