Examples and testing (v0.5.0-beta)
Three equivalent builds of the same circuit on different platforms: a potentiometer that drives a redstone level inside Minecraft, and a physical LED whose brightness is decided by in-game redstone.
The circuit is identical in all three cases; only the transport changes.
| Platform | Transport | Difficulty |
|---|---|---|
| Arduino Uno R3 | USB Serial | Simplest, start here |
| ESP32 | Wi-Fi (TCP) | Cable-free, needs a token |
| Arduino Uno Q | Wi-Fi via MPU + Python | Most involved |
1. Shared setup in Minecraft
All three examples use the same two Hardware I/O Modules:
| Module | Channel | Data direction | Signal type | What it does |
|---|---|---|---|---|
| Sensor | pot_val | Hardware → Minecraft | Analog | Potentiometer powers redstone through a red output face |
| LED | led_verde | Minecraft → Hardware | Analog | Redstone on a green input face controls LED brightness |
The block ID is not translated
The sketches use led_verde (Spanish for "green LED") as the literal key. Channel is a raw protocol string, not a localized display name — if you change it in the game, change it in the sketch too.
Suggested in-game build:
- Place the
pot_valblock and put a redstone lamp beside it, or a dust line into a comparator to read the level. - Place the
led_verdeblock with a lever or redstone dust feeding one of its configured input sides. - Configure the terminals on the blocks in the world: right-click for green input, Shift + right-click for red output. Use Boards → Configuration for channel, direction and signal, and Diagnostics for RX/TX.
Testing without hardware
Before wiring anything, enable the debug HUD (key bound under Options → Controls → SerialCraft). It shows outgoing and incoming messages, which separates a game-side problem from a wiring problem.
2. Arduino Uno R3 (USB)
Wiring
| Component | Board pin | Notes |
|---|---|---|
| Potentiometer, left leg | 5V | |
| Potentiometer, center leg | A0 | Wiper: this is the signal |
| Potentiometer, right leg | GND | |
| LED anode (long leg) | D9 through a 220 Ω resistor | D9 is PWM-capable |
| LED cathode (short leg) | GND |
In-game setup
Connector Block → Connection tab → select the Uno's port → 115200 baud.
WARNING
The sketch uses Serial.begin(115200). Change one value and you must change the other. Mismatched baud rates raise no error — nothing simply arrives.
Sketch
/*
* SerialCraft USB Bridge — Arduino Uno R3
* ============================================================
* Mod SerialCraft v0.4.6 (Minecraft Fabric 26.2, Java 25)
* Comunicacion directa por cable USB (Serial).
*
* Hardware:
* - Potenciometro en A0 (pin central; extremos a 5V y GND)
* - LED verde en D9 (pin PWM) con resistencia de 220 ohm a GND
*
* Bloques IO que debes crear en el juego:
* - Target Data "pot_val" -> modo INPUT (placa -> Minecraft)
* - Target Data "led_verde" -> modo OUTPUT (Minecraft -> placa)
*
* Protocolo y Canales:
* - Canal Bloques IO:
* Enviar al mod: "pot_val:<0-255>\n"
* Recibir del mod: "led_verde:<0-255>\n"
* - Telemetria del juego:
* Prefijo reservado "mc_" (ej. "mc_health:20\n", "mc_damage:4\n").
* Permite descarte inmediato si la placa solo maneja pines.
*
* IMPORTANTE: los baudios de este sketch y los del Bloque Conector en el
* juego deben coincidir. Aqui usamos 115200, que es el valor por defecto
* del mod. Si los cambias en uno, cambialos en el otro.
*/
// ── Pines ────────────────────────────────────────────────────────────
const int POT_PIN = A0;
const int LED_PIN = 9;
// ── Identificadores de bloque (deben coincidir con Target Data) ──
const char* BLOCK_ID_POT = "pot_val";
const char* BLOCK_ID_LED = "led_verde";
// ── Protocolo ────────────────────────────────────────────────────────
const long BAUD_RATE = 115200; // debe coincidir con el Bloque Conector
const int ADC_MAX = 1023; // ADC de 10 bits del ATmega328P
const int PWM_MAX = 255;
const int POT_HYSTERESIS = 2; // ignora ruido electrico del pot
const int BUFFER_LIMIT = 48; // el mod corta lineas de mas de 256
const unsigned long LOOP_DELAY_MS = 30; // ~33 msg/s < limite de 40/s del mod
// ── Estado ───────────────────────────────────────────────────────────
int lastPotValue = -1;
String inputBuffer = "";
void setup() {
Serial.begin(BAUD_RATE);
pinMode(LED_PIN, OUTPUT);
analogWrite(LED_PIN, 0); // LED apagado al iniciar
// Reservar memoria del buffer evita fragmentacion del heap en el Uno.
inputBuffer.reserve(BUFFER_LIMIT + 8);
}
void loop() {
readPotentiometer();
readIncomingCommands();
// Pausa que estabiliza la lectura del ADC y mantiene el ritmo de envio
// por debajo del limitador de red del mod (40 paquetes/s sostenidos).
delay(LOOP_DELAY_MS);
}
// ── 1. Potenciometro -> Minecraft ──────────────────────────
void readPotentiometer() {
int raw = analogRead(POT_PIN);
int potValue = map(raw, 0, ADC_MAX, 0, PWM_MAX);
// Enviar solo si cambio de verdad. Sin esta comprobacion el ruido del
// potenciometro genera un paquete por vuelta de loop().
if (abs(potValue - lastPotValue) >= POT_HYSTERESIS) {
Serial.print(BLOCK_ID_POT);
Serial.print(':');
Serial.println(potValue); // println: el mod ignora lineas sin '\n'
lastPotValue = potValue;
}
}
// ── 2. Minecraft -> LED ────────────────────────────────────
void readIncomingCommands() {
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '\n') {
processCommand(inputBuffer);
inputBuffer = "";
continue;
}
if (c == '\r') continue;
// Cortar ANTES de anadir: con 2 KB de RAM, una linea sin '\n' agota
// la memoria del Uno y lo reinicia en bucle.
if (inputBuffer.length() >= BUFFER_LIMIT) {
inputBuffer = "";
continue;
}
inputBuffer += c;
}
}
void processCommand(String command) {
command.trim();
if (command.length() == 0) return;
// 1. Discriminacion de telemetria:
// Si comienza por "mc_", pertenece a la telemetria del juego (mc_time, mc_health...).
// Al no procesar telemetria en este sketch, lo descartamos de inmediato sin procesar.
if (command.startsWith("mc_")) return;
// 2. Canal de bloques IO (<TARGET_DATA>:<VALOR>)
int sep = command.indexOf(':');
if (sep <= 0 || sep == command.length() - 1) return; // "cmd", ":5" o "cmd:"
String blockId = command.substring(0, sep);
String valueStr = command.substring(sep + 1);
if (blockId == BLOCK_ID_LED) {
// Desde v0.4.3 el mod envia 0-255 tanto en modo Analogico como en
// Digital (digital = 0 o 255), asi que un unico analogWrite() sirve
// para los dos casos sin saber como esta configurado el bloque.
int pwm = constrain(valueStr.toInt(), 0, PWM_MAX);
analogWrite(LED_PIN, pwm);
}
}What to expect
- Turning the potentiometer moves the
pot_valblock's redstone level between 0 and 15. - Feeding redstone into
led_verdechanges the LED brightness proportionally. - The Laptop console shows lines in both directions.
3. ESP32 (Wi-Fi)
3.3 V
The ESP32 is not 5 V tolerant. Power the potentiometer from 3V3.
Wiring
| Component | Board pin | Notes |
|---|---|---|
| Potentiometer, left leg | 3V3 | Never 5 V |
| Potentiometer, center leg | GPIO34 | ADC1, input only |
| Potentiometer, right leg | GND | |
| LED anode | GPIO16 through a 220 Ω resistor | LEDC channel |
| LED cathode | GND |
Why GPIO34
ADC2 is unusable while Wi-Fi is active on the ESP32. Always use ADC1 pins (32-39) in connected projects.
In-game setup
Laptop → Home → Start Wi-Fi server. Copy the IP, port and token shown in the UI into the sketch.
Sketch
/*
* SerialCraft Wi-Fi Bridge — ESP32
* ============================================================
* Mod SerialCraft v0.4.6 (Minecraft Fabric 26.2, Java 25)
*
* DIRECCION DE LA CONEXION (cambio importante en v0.4.3):
* El MOD es el servidor TCP y la PLACA es el cliente.
* Antes se documentaba al reves (ESP32 como servidor en el 8080).
* Ahora: abre la Laptop en el juego -> pestana Inicio -> "Iniciar
* servidor Wi-Fi". La UI te muestra la IP, el puerto y un token.
* Copia esos tres valores aqui abajo.
*
* Handshake: la primera vez, la primera linea que envia la placa es el
* token. El mod responde "OK" o cierra con "ERR TOKEN".
*
* PLACA RECORDADA (sin token): tras conectar, la placa se anuncia con
* mc_id:model=<chip>;uid=<id>
* y, si en el juego pulsas "Recordar placa", el mod le entrega una clave
* (mc_key:...) que se guarda en la memoria flash (NVS). Desde entonces la
* placa entra con "TRUST <uid>" + reto-respuesta HMAC-SHA256 y ya NO necesita
* el token. La clave no vuelve a viajar por la red. Si el mod la olvida
* (ERR UNKNOWN / ERR TRUST) la placa borra su copia y vuelve a usar el token.
*
* Canales de comunicacion:
* - Canal de Bloques IO: "<id>:[0-255]\n"
* - Telemetria del juego: "mc_<clave>:[valor]\n"
*
* Hardware:
* - Potenciometro en GPIO34 (ADC1, solo entrada; extremos a 3V3 y GND)
* - LED verde en GPIO16 con resistencia de 220 ohm a GND
*
* ATENCION 3,3 V: el ESP32 NO tolera 5 V en sus pines. Alimenta el
* potenciometro desde 3V3, nunca desde el pin de 5 V.
*
* Bloques IO que debes crear en el juego:
* - Target Data "pot_val" -> modo INPUT
* - Target Data "led_verde" -> modo OUTPUT
*/
#include <WiFi.h>
#include <Preferences.h>
#include "mbedtls/md.h"
// ═════════════════════════════════════════════════════════════
// CONFIGURACION <- EDITA ESTO
// ═════════════════════════════════════════════════════════════
const char* WIFI_SSID = "TU_WIFI";
const char* WIFI_PASSWORD = "TU_PASSWORD";
const char* MINECRAFT_IP = "192.168.1.50"; // IP que muestra la Laptop
const uint16_t MINECRAFT_PORT = 25585; // puerto por defecto del mod
const char* PAIRING_TOKEN = "XXXXXX"; // token que muestra la Laptop
// ═════════════════════════════════════════════════════════════
const char* BLOCK_ID_POT = "pot_val";
const char* BLOCK_ID_LED = "led_verde";
const int POT_PIN = 34;
const int LED_PIN = 16;
const int ADC_MAX = 4095; // ADC de 12 bits del ESP32
const int PWM_MAX = 255;
const int POT_HYSTERESIS = 3; // el ADC del ESP32 es ruidoso
const unsigned long POT_INTERVAL_MS = 50; // 20 lecturas/s < 40 msg/s
const unsigned long RECONNECT_DELAY_MS = 3000;
const size_t MAX_LINE = 256; // el mod corta la sesion si se excede
WiFiClient client;
Preferences prefs; // memoria flash: guarda la clave recordada
char boardUid[24]; // p. ej. ESP32-A1B2C3D4E5F6 (sale de la MAC)
int lastPotValue = -1;
String rxBuffer = "";
unsigned long lastPotRead = 0;
// ── PWM: la API cambio entre el core 2.x y el 3.x del ESP32 ──
void ledSetup() {
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcAttach(LED_PIN, 5000, 8); // pin, 5 kHz, 8 bits
#else
ledcSetup(0, 5000, 8);
ledcAttachPin(LED_PIN, 0);
#endif
}
void ledWrite(int value) {
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcWrite(LED_PIN, value);
#else
ledcWrite(0, value);
#endif
}
void setup() {
Serial.begin(115200); // solo para depurar por el monitor serie
prefs.begin("serialcraft", false);
snprintf(boardUid, sizeof(boardUid), "ESP32-%012llX", (unsigned long long)ESP.getEfuseMac());
ledSetup();
ledWrite(0);
rxBuffer.reserve(MAX_LINE + 8);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Conectando a Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print('.');
}
Serial.print("\nIP de la placa: ");
Serial.println(WiFi.localIP());
}
void loop() {
if (!client.connected()) {
ledWrite(0); // no dejar el actuador encendido sin enlace
connectToMod();
return;
}
readIncoming();
sendPotentiometer();
}
// ── Identidad y clave ────────────────────────────────────────
// Dice al mod que placa es: "ESP32-S3", "ESP32-C3"... (ESP.getChipModel()).
void announce() {
client.print(String("mc_id:model=") + ESP.getChipModel() + ";uid=" + boardUid + "\n");
}
String hmacSha256Hex(const String& key, const String& msg) {
unsigned char out[32];
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1);
mbedtls_md_hmac_starts(&ctx, (const unsigned char*)key.c_str(), key.length());
mbedtls_md_hmac_update(&ctx, (const unsigned char*)msg.c_str(), msg.length());
mbedtls_md_hmac_finish(&ctx, out);
mbedtls_md_free(&ctx);
const char* hex = "0123456789abcdef";
String result;
result.reserve(64);
for (int i = 0; i < 32; i++) {
result += hex[out[i] >> 4];
result += hex[out[i] & 0x0F];
}
return result;
}
// Lee una linea del mod con limite de tiempo; "" si no llega nada.
String readLineTimeout(unsigned long ms) {
unsigned long deadline = millis() + ms;
while (client.connected() && !client.available() && millis() < deadline) delay(10);
String line = client.readStringUntil('\n');
line.trim();
return line;
}
// ── Conexion + handshake ─────────────────────────────────────
bool handshakeWithToken() {
client.print(String(PAIRING_TOKEN) + "\n");
String reply = readLineTimeout(3000);
if (reply != "OK") {
Serial.println("Handshake rechazado: " + reply + " (revisa el token)");
return false;
}
return true;
}
bool handshakeWithKey(const String& key) {
client.print(String("TRUST ") + boardUid + "\n");
String chal = readLineTimeout(3000);
if (!chal.startsWith("CHAL ")) {
if (chal == "ERR UNKNOWN") {
Serial.println("El mod ya no recuerda esta placa. Pon el token nuevo.");
prefs.remove("key");
}
return false;
}
client.print(hmacSha256Hex(key, chal.substring(5)) + "\n");
String reply = readLineTimeout(3000);
if (reply == "OK") return true;
if (reply == "ERR TRUST") {
Serial.println("Clave rechazada: se borra. Pon el token nuevo.");
prefs.remove("key");
}
return false;
}
void connectToMod() {
Serial.printf("Conectando a %s:%u ...\n", MINECRAFT_IP, MINECRAFT_PORT);
if (!client.connect(MINECRAFT_IP, MINECRAFT_PORT, 5000)) {
Serial.println("Sin respuesta. ¿Entraste al mundo? (el servidor Wi-Fi arranca solo con placas recordadas)");
delay(RECONNECT_DELAY_MS);
return;
}
String key = prefs.getString("key", "");
bool ok = key.length() >= 16 ? handshakeWithKey(key) : handshakeWithToken();
if (!ok) {
client.stop();
delay(RECONNECT_DELAY_MS);
return;
}
Serial.println(key.length() >= 16 ? "Enlazado con SerialCraft (placa recordada)."
: "Enlazado con SerialCraft.");
announce(); // el mod muestra el modelo real de la placa
rxBuffer = "";
lastPotValue = -1; // forzar el primer envio
}
// ── Minecraft -> LED ─────────────────────────────────────────
void readIncoming() {
while (client.available()) {
char c = client.read();
if (c == '\n') {
processCommand(rxBuffer);
rxBuffer = "";
continue;
}
if (c == '\r') continue;
if (rxBuffer.length() >= MAX_LINE) { // linea abusiva: descartar
rxBuffer = "";
continue;
}
rxBuffer += c;
}
}
void processCommand(String command) {
command.trim();
if (command.length() == 0) return;
// 0. Mensajes del propio mod sobre la identidad de esta placa.
if (command.startsWith("mc_key:")) { // clave para reconectar sin token
String newKey = command.substring(7);
if (newKey.length() >= 16 && newKey.length() <= 128) {
prefs.putString("key", newKey);
Serial.println("Clave guardada: esta placa ya no necesita token.");
}
return;
}
if (command.startsWith("mc_who:")) { announce(); return; }
// 1. Discriminacion de telemetria:
// Si comienza por "mc_", es telemetria del juego (mc_time, mc_health, etc.).
// Si la placa solo controla pines fisicos de redstone, se descarta directamente.
if (command.startsWith("mc_")) return;
// 2. Canal de bloques IO (<id>:[0-255])
int sep = command.indexOf(':');
if (sep <= 0 || sep == command.length() - 1) return;
String blockId = command.substring(0, sep);
String valueStr = command.substring(sep + 1);
if (blockId == BLOCK_ID_LED) {
// 0-255 siempre, tanto en Digital (0 o 255) como en Analogico.
int pwm = constrain(valueStr.toInt(), 0, PWM_MAX);
ledWrite(pwm);
}
}
// ── Potenciometro -> Minecraft ───────────────────────────────
void sendPotentiometer() {
if (millis() - lastPotRead < POT_INTERVAL_MS) return;
lastPotRead = millis();
int potValue = map(analogRead(POT_PIN), 0, ADC_MAX, 0, PWM_MAX);
if (abs(potValue - lastPotValue) >= POT_HYSTERESIS) {
client.print(String(BLOCK_ID_POT) + ":" + String(potValue) + "\n");
lastPotValue = potValue;
}
}Verification
Open the serial monitor at 115200: you will see the board's assigned IP, the connection attempt and the handshake result. Handshake rechazado means a wrong token, or another board is already connected.
4. Arduino Uno Q (Bridge + Python)
The Uno Q carries two processors: an STM32U585 microcontroller driving the pins, and a Qualcomm MPU running Linux handling the network. The sketch exposes functions; Python calls them and talks to Minecraft.
MCU (STM32U585) <-> Bridge <-> MPU (Python) <-> Wi-Fi TCP <-> SerialCraft mod3.3 V
The Uno Q's inputs run at 3.3 V. Power the potentiometer from 3V3.
Wiring
| Component | Board pin | Notes |
|---|---|---|
| Potentiometer, left leg | 3V3 | |
| Potentiometer, center leg | A0 | 12-bit ADC |
| Potentiometer, right leg | GND | |
| LED anode | D9 through a 220 Ω resistor | PWM |
| LED cathode | GND |
Sketch (MCU side)
/*
* SerialCraft Wi-Fi Bridge — Arduino Uno Q (lado MCU)
* ============================================================
* Mod SerialCraft v0.4.3 (Minecraft Fabric 1.21.11)
*
* Arquitectura:
* MCU (STM32U585) <-> Bridge <-> MPU (QRB2210 / Python) <-> TCP <-> Mod
*
* Este sketch NO habla con Minecraft. Solo expone funciones al Python
* que corre en la MPU; toda la logica de red vive alli (main.py).
*
* Hardware:
* - Potenciometro en A0 (extremos a 3V3 y GND)
* - LED verde en D9 (PWM) con resistencia de 220 ohm a GND
*
* ATENCION 3,3 V: las entradas del Uno Q trabajan a 3,3 V. Alimenta el
* potenciometro desde 3V3, no desde 5 V.
*/
#include <Arduino_RouterBridge.h>
// ── Pines ──────────────────────────────────────────────────
const int POT_PIN = A0;
const int LED_PIN = 9;
// ── Escalas ────────────────────────────────────────────────
// El ADC del STM32U585 es de 12 bits. La resolucion se fija de forma
// EXPLICITA en setup() para que este valor sea cierto: por defecto el
// core de Arduino devuelve 10 bits, y mapear 0-1023 leyendo 0-4095 hacia
// que el potenciometro llegara al maximo en un cuarto de su recorrido.
const int ADC_BITS = 12;
const int ADC_MAX = 4095;
const int PWM_MAX = 255;
volatile int ledBrightness = 0; // ultimo valor PWM aplicado (0-255)
// ── Funciones expuestas a Python ───────────────────────────
/** Valor crudo del ADC (0-4095). Util para depurar. */
int get_pot_raw() {
return analogRead(POT_PIN);
}
/** Valor del potenciometro en la escala del mod (0-255). */
int get_pot_value() {
return map(analogRead(POT_PIN), 0, ADC_MAX, 0, PWM_MAX);
}
/** Aplica al LED el PWM recibido del mod. */
void set_led_pwm(int pwmValue) {
ledBrightness = constrain(pwmValue, 0, PWM_MAX);
analogWrite(LED_PIN, ledBrightness);
}
/** Brillo actual del LED. */
int get_led_brightness() {
return ledBrightness;
}
// ── Setup ──────────────────────────────────────────────────
void setup() {
pinMode(LED_PIN, OUTPUT);
analogWrite(LED_PIN, 0);
analogReadResolution(ADC_BITS); // sin esto ADC_MAX no seria 4095
Bridge.begin();
Bridge.provide("get_pot_raw", get_pot_raw);
Bridge.provide("get_pot_value", get_pot_value);
Bridge.provide("set_led_pwm", set_led_pwm);
Bridge.provide("get_led_brightness", get_led_brightness);
}
// ── Loop ───────────────────────────────────────────────────
void loop() {
Bridge.update(); // toda la logica Wi-Fi vive en Python
delay(10);
}The ADC detail
analogReadResolution(12) is mandatory. Without it the core returns 10 bits, and mapping from 4095 makes the potentiometer hit maximum a quarter of the way through its travel.
Script (MPU side, Python)
"""
SerialCraft Wi-Fi Bridge — Arduino Uno Q (lado MPU / Python)
============================================================
Mod SerialCraft v0.4.3 (Minecraft Fabric 1.21.11)
Corre en el Qualcomm QRB2210 (Linux) del Arduino Uno Q y se conecta como
CLIENTE TCP al servidor Wi-Fi que levanta el mod desde la Laptop.
Flujo:
1. Conecta y envia el token de emparejamiento (primera linea obligatoria).
2. Espera "OK" del mod. Si llega "ERR TOKEN", el token es incorrecto.
3. Lee el potenciometro via Bridge y envia "pot_val:<0-255>\\n".
4. Recibe "led_verde:<0-255>\\n" y aplica el PWM al LED.
Escala: desde v0.4.3 el cable usa 0-255 en AMBOS sentidos, tanto si el
Bloque IO esta en modo Analogico como en Digital (digital = 0 o 255).
"""
import socket
import threading
import time
from arduino.app_utils import Bridge
# ═══════════════════════════════════════════════════════════════
# CONFIGURACION <- EDITA ESTO ANTES DE EJECUTAR
# ═══════════════════════════════════════════════════════════════
MINECRAFT_IP = "192.168.1.50" # IP que muestra la Laptop en el juego
MINECRAFT_PORT = 25585 # puerto por defecto del mod
PAIRING_TOKEN = "XXXXXX" # token que muestra la Laptop
BLOCK_ID_POT = "pot_val" # Bloque IO en modo INPUT
BLOCK_ID_LED = "led_verde" # Bloque IO en modo OUTPUT
POT_POLL_INTERVAL = 0.05 # 20 lecturas/s (limite del mod: 40 msg/s)
POT_HYSTERESIS = 2 # ignora el ruido del ADC
RECONNECT_DELAY = 3.0
MAX_LINE_LENGTH = 256 # el mod corta la sesion si se excede
RX_BUFFER_LIMIT = 4096
# ═══════════════════════════════════════════════════════════════
_sock: socket.socket | None = None
_sock_lock = threading.Lock()
_stop = threading.Event()
_last_pot_value = -1
# ── Envio ──────────────────────────────────────────────────────
def send_to_mod(message: str) -> bool:
"""Envia una linea al mod. Devuelve False si el enlace ya no sirve."""
with _sock_lock:
sock = _sock
if sock is None:
return False
try:
sock.sendall((message.strip() + "\n").encode("utf-8"))
return True
except OSError as exc:
print(f"[WiFi] Error enviando: {exc}")
return False
# ── Recepcion ──────────────────────────────────────────────────
def receive_loop(connection: socket.socket) -> None:
buf = ""
while not _stop.is_set():
try:
data = connection.recv(1024)
except OSError as exc:
print(f"[WiFi] Error recibiendo: {exc}")
break
if not data:
print("[WiFi] El mod cerro la conexion.")
break
buf += data.decode("utf-8", errors="replace")
# Sin '\n' a la vista, el buffer crece sin limite: cortarlo.
if len(buf) > RX_BUFFER_LIMIT:
print("[WiFi] Buffer sin terminador. Descartando.")
buf = ""
continue
while "\n" in buf:
line, buf = buf.split("\n", 1)
line = line.strip()
if line and len(line) <= MAX_LINE_LENGTH:
process_mod_message(line)
def process_mod_message(line: str) -> None:
"""Interpreta BLOCK_ID:VALOR y aplica la accion en el MCU."""
block_id, sep, value_str = line.partition(":")
if not sep:
print(f"[Mod] Mensaje sin separador: {line!r}")
return
block_id = block_id.strip()
value_str = value_str.strip()
if block_id != BLOCK_ID_LED:
print(f"[Mod] Bloque desconocido: {block_id!r}")
return
try:
pwm = int(value_str)
except ValueError:
print(f"[Mod] Valor no numerico para el LED: {value_str!r}")
return
pwm = max(0, min(255, pwm))
Bridge.call("set_led_pwm", pwm)
print(f"[Bridge] LED -> PWM={pwm}")
# ── Potenciometro ──────────────────────────────────────────────
def potentiometer_loop() -> None:
global _last_pot_value
while not _stop.is_set():
try:
pot_level = int(Bridge.call("get_pot_value"))
except Exception as exc: # noqa: BLE001
print(f"[Bridge] Error leyendo el potenciometro: {exc}")
time.sleep(POT_POLL_INTERVAL)
continue
# Enviar solo cambios reales: sin esto el ruido del ADC agota el
# limitador de red del mod (40 paquetes/s sostenidos).
if abs(pot_level - _last_pot_value) >= POT_HYSTERESIS:
if send_to_mod(f"{BLOCK_ID_POT}:{pot_level}"):
print(f"[Bridge->Mod] {BLOCK_ID_POT}:{pot_level}")
_last_pot_value = pot_level
else:
_stop.set() # forzar reconexion
return
time.sleep(POT_POLL_INTERVAL)
# ── Sesion ─────────────────────────────────────────────────────
def handshake(connection: socket.socket) -> bool:
"""Envia el token y espera el 'OK' del mod."""
connection.sendall((PAIRING_TOKEN + "\n").encode("utf-8"))
connection.settimeout(5)
try:
reply = connection.recv(64).decode("utf-8", errors="replace").strip()
except OSError:
print("[WiFi] El mod no respondio al handshake.")
return False
finally:
connection.settimeout(None)
if reply.splitlines()[:1] != ["OK"]:
print(f"[WiFi] Handshake rechazado ({reply!r}). Revisa el token.")
return False
return True
def connect_and_run() -> None:
global _sock, _last_pot_value
print(f"[WiFi] Conectando a {MINECRAFT_IP}:{MINECRAFT_PORT} ...")
connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connection.settimeout(10)
try:
connection.connect((MINECRAFT_IP, MINECRAFT_PORT))
connection.settimeout(None)
if not handshake(connection):
return
print("[WiFi] Enlazado con SerialCraft.")
_stop.clear()
_last_pot_value = -1
with _sock_lock:
_sock = connection
pot_thread = threading.Thread(
target=potentiometer_loop, daemon=True, name="SerialCraft-POT"
)
pot_thread.start()
receive_loop(connection) # bloquea hasta la desconexion
except OSError as exc:
print(f"[WiFi] No se pudo conectar: {exc}")
finally:
_stop.set()
with _sock_lock:
_sock = None
try:
connection.close()
except OSError:
pass
# No dejar el actuador encendido sin enlace.
try:
Bridge.call("set_led_pwm", 0)
except Exception: # noqa: BLE001
pass
print("[WiFi] Socket cerrado.")
def main() -> None:
print("=" * 55)
print(" SerialCraft Wi-Fi Bridge — Arduino Uno Q")
print(f" Mod en: {MINECRAFT_IP}:{MINECRAFT_PORT}")
print(f" Bloque pot: {BLOCK_ID_POT}")
print(f" Bloque LED: {BLOCK_ID_LED}")
print("=" * 55)
try:
Bridge.call("set_led_pwm", 0)
except Exception: # noqa: BLE001
pass
while True:
connect_and_run()
print(f"[WiFi] Reintentando en {RECONNECT_DELAY}s ...\n")
time.sleep(RECONNECT_DELAY)
if __name__ == "__main__":
main()5. Test routine
Run these five steps in order. Each isolates a different layer, so you know exactly where a failure sits.
| # | Test | Expected result | If it fails |
|---|---|---|---|
| 1 | No Minecraft, serial monitor open: turn the potentiometer | pot_val:<n> lines appear | Potentiometer wiring or wrong pin |
| 2 | Type led_verde:255 by hand in the serial monitor | LED lights fully | Resistor, LED polarity, or a non-PWM pin |
| 3 | Connect from the game | The Laptop shows a green link | Baud mismatch (USB) or wrong token (Wi-Fi) |
| 4 | Turn the potentiometer with the game open | The pot_val block emits redstone | Target Data typo, or the block is not in INPUT |
| 5 | Power led_verde with a lever | The LED lights | The block is not in OUTPUT, or is disabled from the Laptop |
Shortcut: test the LED without redstone
Step 5 can be done without a lever: open Visualizer, leave led_verde as the generator key and press Generate. The ramp should look like a brightness fade on the LED, and the orange led_verde lane on the timeline shows exactly what the board receives. If the LED responds here but not with redstone, the fault is in the block (mode, Target Data or sides), not in the board. Details in the test bench.
Testing digital mode
Switch the led_verde block to Digital and power it again with the lever. The LED must light fully, not faintly. That is the v0.4.3 fix: digital mode sends 255, not 1.
If the LED is barely visible, you are running a 0.3.x sketch that compares value == 1.
Testing the rate limit
Temporarily remove the hysteresis so the sketch transmits on every loop() pass. The game will start ignoring some changes: the limiter is dropping the excess. That is correct behaviour. Put the hysteresis back.
The Sensor view of the Visualizer tab tells you how many messages per second your sketch sends and which deadband suits your potentiometer's real noise.