Skip to content

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.

PlatformTransportDifficulty
Arduino Uno R3USB SerialSimplest, start here
ESP32Wi-Fi (TCP)Cable-free, needs a token
Arduino Uno QWi-Fi via MPU + PythonMost involved

1. Shared setup in Minecraft ​

All three examples use the same two Hardware I/O Modules:

ModuleChannelData directionSignal typeWhat it does
Sensorpot_valHardware → MinecraftAnalogPotentiometer powers redstone through a red output face
LEDled_verdeMinecraft → HardwareAnalogRedstone 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:

  1. Place the pot_val block and put a redstone lamp beside it, or a dust line into a comparator to read the level.
  2. Place the led_verde block with a lever or redstone dust feeding one of its configured input sides.
  3. 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 ​

ComponentBoard pinNotes
Potentiometer, left leg5V
Potentiometer, center legA0Wiper: this is the signal
Potentiometer, right legGND
LED anode (long leg)D9 through a 220 Ω resistorD9 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 ​

cpp
/*
 * 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_val block's redstone level between 0 and 15.
  • Feeding redstone into led_verde changes 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 ​

ComponentBoard pinNotes
Potentiometer, left leg3V3Never 5 V
Potentiometer, center legGPIO34ADC1, input only
Potentiometer, right legGND
LED anodeGPIO16 through a 220 Ω resistorLEDC channel
LED cathodeGND

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 ​

cpp
/*
 * 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.

text
MCU (STM32U585) <-> Bridge <-> MPU (Python) <-> Wi-Fi TCP <-> SerialCraft mod

3.3 V

The Uno Q's inputs run at 3.3 V. Power the potentiometer from 3V3.

Wiring ​

ComponentBoard pinNotes
Potentiometer, left leg3V3
Potentiometer, center legA012-bit ADC
Potentiometer, right legGND
LED anodeD9 through a 220 Ω resistorPWM
LED cathodeGND

Sketch (MCU side) ​

cpp
/*
 * 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) ​

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.

#TestExpected resultIf it fails
1No Minecraft, serial monitor open: turn the potentiometerpot_val:<n> lines appearPotentiometer wiring or wrong pin
2Type led_verde:255 by hand in the serial monitorLED lights fullyResistor, LED polarity, or a non-PWM pin
3Connect from the gameThe Laptop shows a green linkBaud mismatch (USB) or wrong token (Wi-Fi)
4Turn the potentiometer with the game openThe pot_val block emits redstoneTarget Data typo, or the block is not in INPUT
5Power led_verde with a leverThe LED lightsThe 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.