Skip to content

Bidirectional Protocol and Hardware (v0.4.6) ​

Beta 0.4.6

This specification describes 0.4.6-beta. The IO module documents the new connector and logic rules. Historical changes to the 0.3.x scale and Wi-Fi direction remain below.

1. Communication specification ​

SerialCraft uses a plain-text (UTF-8), line-oriented protocol. Every message must end with \n, over USB and over TCP alike.

ParameterValue
Supported baud rates (Serial)300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 74880, 115200 (default), 230400, 250000
TCP port (Wi-Fi)25585 (default)
FormatKEY:VALUE
Terminator\n
EncodingUTF-8
Maximum line length256 characters
Maximum Target Data length32 characters
Maximum inbound rate40 messages/s sustained, burst of 80

The terminator is not optional

The mod ignores anything that does not end with \n. Always use Serial.println() or client.print("...\n"), never a bare print().

Why the 40 messages/s cap exists

Valid IO samples enter a bounded queue per channel; up to two queued samples per tick become server packets. An unthrottled Serial.println() inside loop() produces thousands per second and causes real lag on a multiplayer server. Excess packets are dropped silently — that is backpressure, not an error. Send on changes, plus occasional snapshots (the examples resend sensors once per second to restore state after chunk reloads). This does not imply a connection heartbeat or a delivery acknowledgement.


2. Unified 0-255 scale ​

Since v0.4.3 the wire always speaks 0-255, in both directions and for both signal types. Redstone inside Minecraft is still 0-15; the mod does the conversion.

Signal typeMinecraft → boardBoard → Minecraft
Analog (PWM)redstone × 255 / 15value × 15 / 255
Digital0 if redstone = 0, 255 if redstone ≥ 10 if value = 0, 15 if value ≥ 1

Analog conversion table ​

RedstoneWire valueDescription
00Off
117Minimum
7119Mid (~50%)
15255Maximum

Practical consequence

Your firmware does not need to know whether the block is Digital or Analog. A single analogWrite(pin, value) covers both. In 0.3.x digital mode sent 1, and analogWrite(pin, 1) left the LED effectively dark at a 0.4% duty cycle — the classic first-circuit failure.

In digital mode, any incoming value of 1 or greater is treated as fully on (redstone 15). You may send 1, 15 or 255 interchangeably.


3. Input (Hardware ➔ Minecraft) ​

The board sends the target block ID and a value:

text
<TARGET_DATA>:<INTEGER>\n
  • <TARGET_DATA>: the Channel field (formerly Target Data), e.g. btn_1 or light_sensor. Use 1–32 ASCII letters, digits, _, . or -; mc_ is reserved and an empty channel is rejected.
  • <INTEGER>: 0-255. Out-of-range values are rejected; non-numeric text is discarded without throwing.
cpp
Serial.println("light_sensor:200");   // ~redstone 12 in analog mode
Serial.println("alarm:255");          // fully on
Serial.println("alarm:0");            // off

The IO Block must be in INPUT mode with a matching Target Data. Only the blocks of the player holding the connection receive messages.


4. Output (Minecraft ➔ Hardware) ​

An IO Block in OUTPUT mode emits automatically whenever the redstone level it receives changes:

text
<TARGET_DATA>:<VALUE>\n

Two implementation details worth knowing:

  • Deduplication: an unchanged value is not resent continuously; connection/session changes force resynchronization. Holding a lever on sends one message, not twenty per second.
  • Interval: world changes are grouped once per tick; stable inputs are not polled periodically. This bounds server work while preserving ordinary redstone transitions.

5. Wi-Fi: the mod is the server ​

Direction changed from 0.3.x

Previously the board opened a server on port 8080 and Minecraft connected to it. It is now the other way around: the Minecraft client opens the TCP server and the board connects as a client.

Connection sequence ​

  1. In game: Laptop → Home → Start Wi-Fi server. The UI shows the local IP, the port (25585) and a pairing token.
  2. The board opens a TCP connection to that IP and port.
  3. The board sends the token as its first line, terminated with \n.
  4. The mod replies OK\n if correct, or ERR TOKEN\n and closes.
  5. From then on the channel carries normal KEY:VALUE messages.
text
board → mod: ABC123\n
mod → board: OK\n
board → mod: pot_val:128\n
mod → board: green_led:255\n

Server restrictions ​

RuleReason
Only one board at a timeStops a third party from repeatedly kicking yours off
Private addresses only (LAN, loopback, link-local)Prevents accidental exposure to the internet
Lines over 256 characters end the sessionA peer that never sends \n would exhaust client memory
Token requiredWithout it, anyone on the network could drive your redstone over telnet

The channel is plaintext

The token prevents casual or accidental access, but it is not encryption. Suitable for a home or classroom network. Do not forward port 25585 on your router or use this over the internet.


6. Gate logic ​

In Minecraft → Hardware, OR returns the maximum, AND the minimum (including zero), and XOR the maximum with an odd number of positive inputs, otherwise zero. No inputs sends zero. In Hardware → Minecraft, inputs enable the stored hardware sample; no inputs bypass the gate. A closed condition does not discard RX. See module guide.


7. Common failures ​

SymptomUsual cause
LED stays dark although the game reports sendingOld sketch comparing against 1 in digital mode. It now receives 255; use analogWrite with no comparison
Nothing arrives over USBSketch baud rate differs from the Connector Block's
The board connects and immediately dropsWrong token, or another board is already connected
Only some messages arriveYou are above 40 messages/s. Add hysteresis and send changes only
The block ignores everythingTarget Data empty or different from the message key
The pot maxes out halfway through its travelADC resolution mapped wrong (10-bit vs 12-bit)

8. Changes from 0.3.x ​

  1. Unified scale: input was 0-15 and output 0-255. A 200 sent by the board came back as 15. Both ends now use 0-255.
  2. Digital sends 255, not 1.
  3. Wi-Fi inverted: the mod is the server, the board is the client.
  4. Port 25585 instead of 8080.
  5. Pairing token is mandatory.
  6. Rate limit of 40 messages/s per player.
  7. An empty Target Data no longer accepts any message containing :.

9. Changes from 0.4.3 ​

Version 0.4.6 introduces the game telemetry channel and enforces isolation between block signals and game state:

  1. Reserved mc_ prefix: The mc_ prefix is strictly reserved for in-game telemetry (mc_time, mc_health, mc_damage, etc.).
    • Server-side rejection: The server rejects any attempt to configure an IO Block whose Target Data starts with mc_, warning the player with a clear message (message.serialcraft.reserved_key).
    • Input discard: In ArduinoIOBlockEntity, any message received from the board that begins with mc_ is discarded immediately in $O(1)$ without touching redstone or NBT.
  2. Game telemetry (Minecraft ➔ Hardware): Player and world events are streamed to the microcontroller using the format mc_<channel>:<integer>\n enabled from the Laptop's Events tab.
  3. Pacing and flow control: Outbound telemetry is strictly throttled to at most 1 line per tick (20 lines/s max) via TelemetryOutbox, giving priority to edge events over state snapshots to avoid overflowing the 64-byte serial buffer on boards like the ATmega328P.
  4. Periodic 5-second state resends: Active periodic channels are quietly resent every 100 ticks (5 s) without console spam, ensuring microcontrollers that reset upon opening serial DTR lines acquire current state without manual intervention.
  5. Board identification and token-free reconnection: Optional mc_who, mc_id and mc_key lines, and a second handshake path (TRUST with challenge-response) for remembered boards. See section 12.
  6. Test bench (Visualizer tab): The mod records key:value messages in both directions with real timestamps and can send test waves to the board. It adds nothing to the wire format. See section 13.

10. Full examples ​

Ready-to-flash sketches in Examples and testing: Arduino Uno R3 (USB), ESP32 (Wi-Fi) and Arduino Uno Q (Bridge + Python), with wiring diagrams.


11. Game telemetry (Minecraft ➔ Hardware) ​

New in 0.4.6-beta

The panel's Events tab lets you choose which game data (time of day, hunger, damage taken...) is sent to the board. It is the same USB or Wi-Fi channel as in the previous sections; only the line format differs.

Format ​

text
mc_<channel>:<integer>\n
RuleDetail
Reserved prefix mc_No IO Block may use it as Target Data (the server rejects the configuration with a notice). A board can therefore never mistake game data for a redstone command.
Keymc_[a-z0-9_]{1,29}: lowercase letters, digits and _, 32 characters at most.
ValueSigned decimal integer. Every channel fits a 16-bit int (the size on an Arduino Uno). Each channel also promises its own, narrower range, listed below.
Off by defaultNo channel is sent until you tick it in the tab. An older sketch never receives lines it did not ask for.
Unknown keysYour sketch must ignore them. New channels will be added without breaking existing ones.

Channels ​

KeyKindRangeMeaning
mc_timeState0-23999Time of day in ticks: 0 = 06:00, 6000 = 12:00, 12000 = 18:00, 18000 = 00:00. A day lasts 20 real minutes (20 ticks/s).
mc_isdayState0-11 if mc_time is below 12000.
mc_weatherState0-20 clear, 1 rain, 2 thunderstorm.
mc_healthState0-1024Health in points, rounded (2 points = 1 heart).
mc_hungerState0-20Hunger level.
mc_saturationState0-20Saturation, rounded.
mc_levelState0-32767Experience level.
mc_airState0-100Remaining air, as a percentage.
mc_fireState0-11 while the player is on fire.
mc_damageEvent1-32767Health points lost between two consecutive ticks. It is a net loss: damage absorbed by golden hearts and the cut-back when a bonus-health effect ends do not count.
mc_deathEvent1The player has died. The line appearing is the event; the value is always 1.

mc_time follows the clock of the dimension the player is in. In dimensions without their own day/night cycle it may stay fixed; the channel is meant for the Overworld.

Delivery guarantees ​

StateEvent
When it is sentOn change, at the interval chosen in the tab (0.25 to 10 s), and every 5 s even if unchangedOnce per occurrence
ResentYesNever: repeating it would make the board think there was another hit
If it is lostRecovered at the next resendLost: there is no acknowledgement

What this means for your sketch:

  • Treat states as idempotent. Receiving mc_hunger:20 twice must change nothing.
  • The 5 s resend exists because of USB. An Arduino Uno resets when the computer opens its port, and the first lines land inside the bootloader's startup. Without a resend, a stable value would never arrive.
  • Pacing. At most one line per tick (20 lines/s), with events ahead of states. On connect, the full snapshot takes a few hundred milliseconds. The event queue holds 16; when full, the oldest is dropped.
  • Game data and IO Block outputs share the same channel. Tell them apart by the mc_ prefix.

Example: ESP32 clock with a servo ​

The hand sweeps 0° → 180° during the day and back to 0° at night (a standard servo cannot turn a full circle). Enable Time of day in the Events tab. The board interpolates between messages so the motion is smooth, and stops after 15 s without news so it does not drift while the game is paused.

cpp
#include <WiFi.h>
#include <ESP32Servo.h>

// ── Settings ─────────────────────────────────────────────────
const char*    WIFI_SSID = "TU_WIFI";
const char*    WIFI_PASS = "TU_CLAVE";
const char*    MOD_HOST  = "192.168.1.50";  // IP shown by the mod's Laptop
const uint16_t MOD_PORT  = 25585;
const char*    MOD_TOKEN = "ABC123";        // token shown by the mod's Laptop
const int      SERVO_PIN = 18;              // power the servo from an external 5 V supply

WiFiClient client;
Servo hand;

// Last time received and the local instant it arrived
long          baseTicks  = 0;
unsigned long baseMillis = 0;
bool          haveTime   = false;

int           currentAngle = 0;
unsigned long lastStep     = 0;
unsigned long lastTry      = 0;

// 20 ticks/s = 1 tick every 50 ms. With no news from the mod for 15 s (above the
// tab's maximum interval of 10 s) the hand stops advancing on its own, so it
// does not drift while the game is paused.
long estimatedTicks() {
  long elapsed = (long)((millis() - baseMillis) / 50UL);
  if (elapsed > 300) elapsed = 300;
  return (baseTicks + elapsed) % 24000L;
}

// Day (0-12000): 0° -> 180°.  Night (12000-24000): 180° -> 0°.
int targetAngle(long t) {
  if (t < 12000L) return (int)(t * 180L / 12000L);
  return (int)((24000L - t) * 180L / 12000L);
}

void handleLine(String line) {
  line.trim();
  int sep = line.indexOf(':');
  if (sep <= 0) return;                        // not a KEY:VALUE line
  String key  = line.substring(0, sep);
  long   value = line.substring(sep + 1).toInt();

  if (key == "mc_time") {
    baseTicks  = value;
    baseMillis = millis();
    haveTime   = true;
  }
  // Unknown keys are ignored: the sketch keeps working if you enable more
  // data in the Events tab.
}

bool connectToMod() {
  if (!client.connect(MOD_HOST, MOD_PORT)) return false;
  client.print(String(MOD_TOKEN) + "\n");      // the token is the first line
  String reply = client.readStringUntil('\n');
  reply.trim();
  return reply == "OK";
}

void moveHand() {
  if (millis() - lastStep < 15) return;        // at most 1° every 15 ms
  lastStep = millis();
  int target = targetAngle(estimatedTicks());
  if (currentAngle == target) return;
  currentAngle += (currentAngle < target) ? 1 : -1;
  hand.write(currentAngle);
}

void setup() {
  hand.setPeriodHertz(50);
  hand.attach(SERVO_PIN, 500, 2400);
  hand.write(currentAngle);

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);
}

void loop() {
  if (!client.connected()) {
    if (millis() - lastTry > 2000) {           // retry every 2 s
      lastTry = millis();
      connectToMod();
    }
    return;
  }

  while (client.available()) handleLine(client.readStringUntil('\n'));
  if (haveTime) moveHand();
}

Do not power the servo from the ESP32

Use its own 5 V supply and join the grounds. The ESP32's 3.3 V pin cannot supply a servo's startup current spike.

12. Board identification and token-free reconnection ​

Everything here is optional and backward compatible: a board that knows nothing about it keeps working with the usual token. These lines use the reserved mc_ prefix and are consumed by the client — they never reach the server.

Identification ​

DirectionLineMeaning
mod ➔ boardmc_who:1"Who are you?". Sent 1.5 s, 4 s and 9 s after connecting until answered. Older boards ignore it.
board ➔ modmc_id:model=ESP32-S3;uid=ESP32-A1B2C3D4E5F6Model (≤ 32 chars) and a stable id ([A-Za-z0-9_-], 4–32; e.g. derived from the MAC). Short form mc_id:esp32-s3 also works.

The mod merges several sources and keeps the most reliable: board announcement > exact USB model (VID:PID) or ESP boot banner > vendor > bridge chip. A CH340 or CP2102 does not tell which board is behind it (the same chip ships on Nano clones and ESP32s), so the UI shows "Board with CH340 (model not identified)" until the board announces itself.

Remembered board (no token) ​

  1. First time: the board connects with the token and announces itself with mc_id. A Remember board button appears on the Home tab; pressing it makes the mod generate a 128-bit key, store it in config/serialcraft-boards.properties, and send it as mc_key:<key>. The board saves it in flash.
  2. Later sessions, the first line is TRUST <uid> instead of the token:
board ➔ mod   TRUST ESP32-A1B2C3D4E5F6
mod   ➔ board CHAL 9d1f00aa11bb22cc33dd44ee55ff6677      (random challenge, different every time)
board ➔ mod   <HMAC-SHA256(key, challenge) as lowercase hex>
mod   ➔ board OK

The key (as ASCII hex) is the HMAC key and the challenge (as ASCII hex) is the message. The key never travels again. Errors: ERR UNKNOWN (mod doesn't know that board) and ERR TRUST (wrong answer); in both cases the board should delete its key and fall back to the token.

With remembered boards, the Wi-Fi server starts by itself when you join a world (settings.autoStartWifi), so the board reconnects without opening the Laptop.

The first pairing (token and mc_key) travels in clear text over the LAN, exactly as the token always has: do it on a trusted network. To revoke a board press Forget board or delete its block from the file.


13. Testing with the generator (Visualizer tab) ​

The Visualizer tab's generator sends the board lines in the usual key:value format. It adds nothing to the protocol: your sketch treats them like those from an output IO Block.

PropertyValue
Format<key>:<0-255>\n, integer
RateAt most 20 lines/s (one per tick) and only when the value changed: the same ceiling as the game's telemetry
On stopCancels pending values and requests <key>:0 on the original channel, only within the same connection session
Key[A-Za-z0-9_.-], up to 32 characters; fixed for the duration of the run
ConsoleDoes not write to the console, to avoid flooding it

Wave shapes (period of 1, 2, 5 or 10 s; amplitude of 100, 50 or 25 % of 0-255):

ShapeValues
RampRises from 0 to 255 and drops back to 0 at once
TriangleRises and falls smoothly
Square0 for the first half of the period and 255 for the second
SineSinusoidal, starts at 0
Stairs16 steps: 0, 17, 34… 255, one per redstone level

With an LED on a PWM pin, the ramp should look like a brightness fade and the stairs like 16 distinct levels. If the LED responds to the generator but not to redstone, the fault is in the block (mode, Target Data or sides), not in the board or the wiring.

What the mod records ​

Every key:value line received (RX) or sent (TX) is stored with the real time it happened. The Visualizer tab draws that log.

  • Signed and decimal values are accepted (up to 9 integer digits and 6 decimals) for display only: IO Blocks work with integers from 0 to 255.
  • Lines that are not shaped like key:number are ignored: boot banners, free text and the mc_id: identification lines.
  • Up to 16 series are kept (each direction + key pair) with ~4000 samples per series; when full, the oldest is dropped.
  • Received lines are noted before the rate limiter: you see what the board sent, not what survived the filter.