Resources / IoT / Arduino Dice Roll

Control Arduino with Claude

Wire a NANO ESP32 to a button, publish dice rolls over MQTT, and display them in a real-time SimSense sim — no server required.

Arduino MQTT ESP32 Hardware
01 —

How it Works

When a user presses and releases the button connected to a NANO ESP32, the board generates a random number between 1 and 6, converts it to JSON, and publishes it via MQTT to a public broker. A browser-based simulator subscribes to the same broker and animates the dice in real time without requiring server infrastructure or Arduino Cloud dependency.

The data flow follows this path: button release triggers the microcontroller, which connects via WiFi to broker.hivemq.com using standard MQTT on port 1883. The browser-based sim connects using WebSocket on port 8884 and displays the result.

02 —

Prerequisites

Hardware Required

  • NANO ESP32 — Microcontroller with built-in WiFi capability
  • Momentary button — Any standard 4-pin tactile pushbutton
  • Jumper wires — Two wires connecting button to board and ground
  • Breadboard — Optional but recommended for clean wiring

Wiring

Connect the button using only two wires from D2 to one button leg and GND to the other. No resistor is required because the sketch uses the board's internal pull-up resistor. This approach allows the pin to read HIGH at rest and LOW when the button is pressed, with release triggering the roll action.

Software Requirements

  • Arduino IDE 2.x or Arduino Cloud editor
  • Arduino ESP32 Boards package installed via Boards Manager
  • PubSubClient library by Nick O'Leary installed from Library Manager
  • Existing Arduino IoT Cloud sketch with thingProperties.h
03 —

The Complete Sketch

Upload this to your NANO ESP32. The sketch handles WiFi, MQTT connection, button debouncing, and serial simulation in a single file.

Arduino sketch (.ino) C++
#include "thingProperties.h"
#include 

// ── Dice config ──────────────────────────────────────
const char* MQTT_HOST  = "broker.hivemq.com";
const int   MQTT_PORT  = 1883;
const char* MQTT_TOPIC = "simsense/dice/23672c2e";
const int   BUTTON_PIN = 2;
// ─────────────────────────────────────────────────────

WiFiClient   wifiClient;
PubSubClient mqtt(wifiClient);

int  rollCount   = 0;
bool lastReading = HIGH;
bool btnState    = HIGH;
unsigned long lastDebounce = 0;
const unsigned long DEBOUNCE_MS = 50;

void connectMQTT() {
  String id = "nano-dice-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  Serial.print("MQTT connecting…");
  if (mqtt.connect(id.c_str())) Serial.println(" done");
  else Serial.println(" failed, will retry");
}

void doRoll() {
  rollCount++;
  int roll = random(1, 7);
  String payload = "{\"v\":" + String(roll)
                 + ",\"c\":" + String(rollCount)
                 + ",\"t\":\"\"}";
  if (mqtt.publish(MQTT_TOPIC, payload.c_str(), true))
    Serial.println("Rolled " + String(roll) + " (#" + String(rollCount) + ")");
  else
    Serial.println("Publish failed — will retry next roll");
}

void setup() {
  Serial.begin(9600);
  delay(1500);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  randomSeed(analogRead(0));
  initProperties();
  ArduinoCloud.begin(ArduinoIoTPreferredConnection);
  setDebugMessageLevel(2);
  ArduinoCloud.printDebugInfo();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  Serial.println("Type r + Enter to simulate a button press.");
}

void loop() {
  ArduinoCloud.update();
  if (WiFi.status() == WL_CONNECTED && !mqtt.connected()) connectMQTT();
  mqtt.loop();

  // ── Serial simulation ──────────────────────────────
  if (Serial.available()) {
    char c = Serial.read();
    if (c == 'r' || c == 'R') doRoll();
  }

  // ── Physical button (debounced) ────────────────────
  bool reading = digitalRead(BUTTON_PIN);
  if (reading != lastReading) lastDebounce = millis();
  if ((millis() - lastDebounce) > DEBOUNCE_MS && reading != btnState) {
    btnState = reading;
    if (btnState == HIGH) doRoll();
  }
  lastReading = reading;
}

void onLEDChange() {
  // your existing LED logic here
}
04 —

Section-by-Section Explainer

Includes & Configuration

The sketch includes thingProperties.h, which is auto-generated by Arduino IoT Cloud and contains WiFi credentials and variable definitions. PubSubClient.h provides the MQTT functionality. The configuration section defines the free public broker at broker.hivemq.com using standard unencrypted port 1883, suitable for local demonstrations. The MQTT_TOPIC uses a unique identifier to prevent collisions with other users. BUTTON_PIN is set to GPIO2 (digital pin D2).

Global Variables

WiFiClient creates a low-level TCP socket for PubSubClient to use. The mqtt object wraps this socket for protocol handling. rollCount tracks session button presses, while lastReading and btnState store pin states for debouncing. The debounce logic requires a 50-millisecond stable signal before accepting a state change, filtering mechanical contact bounce.

connectMQTT()

This function generates a unique client ID using the ESP32's burned-in MAC address, ensuring no two boards clash on the broker. It attempts to open a TCP connection and send the MQTT CONNECT packet. The function is called from the loop only after WiFi connectivity is confirmed, preventing blocking during network transitions.

doRoll()

The function increments the roll counter and generates a random integer from 1 to 6 inclusive using Arduino's random() function, which excludes the upper bound. The payload is formatted as a compact JSON object containing the dice value (v), cumulative roll count (c), and an empty timestamp field (t). Publishing with the retained flag set to true causes the broker to cache the message, so new browsers receive the last result immediately.

setup()

Serial communication begins at 9600 baud with a 1500-millisecond delay to allow the Serial Monitor time to attach. INPUT_PULLUP enables the internal pull-up resistor, making the pin rest at 3.3V (HIGH) until the button pulls it to ground (LOW). randomSeed() seeds the random number generator from electrical noise on an unconnected analog pin.

loop()

ArduinoCloud.update() maintains WiFi and cloud variable synchronization on every iteration. The WiFi status check prevents MQTT connection attempts during disconnections. mqtt.loop() keeps the MQTT connection alive by sending keep-alive packets. The debounce logic detects pin changes, waits 50 milliseconds for signal stability, then triggers the roll action when the button is released (HIGH state with INPUT_PULLUP convention).

05 —

The MQTT Payload

Each roll publishes a JSON message:

Payload example JSON
{"v": 4, "c": 7, "t": ""}
  • v — Dice value (1–6)
  • c — Cumulative roll count, displayed as "Roll #N"
  • t — Timestamp, left empty (the board has no RTC; the sim records browser arrival time)

The retained message flag tells the broker to cache this data, ensuring any browser opening the sim after a previous roll sees the last result immediately.

06 —

Testing Without Hardware

You can test the full system before connecting any button by uploading the sketch and opening the Serial Monitor at 9600 baud.

  1. Upload the sketch to your NANO ESP32
  2. Open Serial Monitor at 9600 baud
  3. Wait for MQTT connecting… done to appear
  4. Type r and press Enter to trigger a roll
  5. Watch the simulator animate at the URL in the Quick Reference table
Note

If "Publish failed" appears, the MQTT connection dropped but will auto-reconnect on the next roll attempt.

07 —

Going Live With Hardware

Once serial testing succeeds, connect one button leg to D2 and the other to GND. The button and serial simulation coexist in the same sketch with no code changes required. Pressing and releasing the button triggers rolls visible in both the Serial Monitor and the simulator.

Alternative GPIO pins include D3 (GPIO3) or D4 (GPIO4) if GPIO2 exhibits unexpected behavior on certain board revisions, with corresponding BUTTON_PIN configuration changes.

08 —

Quick Reference

Item Value
Simulator URLhttps://sim-coral-breeze-5800.my.simsense.ai
MQTT Brokerbroker.hivemq.com
Arduino Port1883 (TCP)
Browser Port8884 (WebSocket + TLS)
Topicsimsense/dice/23672c2e
Button PinD2 (GPIO2)
Serial TriggerType r + Enter
Baud Rate9600
Required LibraryPubSubClient by Nick O'Leary
09 —

Simulator Source Code

The simulator is a single HTML file served from simsense.ai that connects to the MQTT broker over WebSocket, subscribes to the dice topic, and animates the dice face upon message arrival.

Dependencies

Script tags HTML

mqtt.min.js is the browser-compatible build of the MQTT.js library, running entirely in-browser over WebSocket. window.SimSense is injected by the simsense.ai platform and provides key-value storage shared across all viewers in real time.

MQTT Connection

Browser MQTT client JavaScript
const TOPIC    = "simsense/dice/23672c2e";
const clientId = "sim-" + Math.random().toString(16).slice(2, 8);

const client = mqtt.connect("wss://broker.hivemq.com:8884/mqtt", {
  clientId,
  clean: true,
  reconnectPeriod: 3000,
  connectTimeout: 10000
});

client.on("connect", () => {
  client.subscribe(TOPIC, { qos: 1 });
});

client.on("message", (topic, message) => {
  const payload = JSON.parse(message.toString());
  applyRoll(payload);
});

Browsers cannot open raw TCP sockets, so MQTT.js upgrades to secure WebSocket (wss://) on port 8884, while the Arduino uses plain TCP on port 1883. Both reach the same broker and topic. Quality of Service level 1 guarantees delivery at least once.

Applying a Roll

Roll handler JavaScript
function applyRoll(payload) {
  const { v, c, t } = payload;

  // Trigger roll animation on the dice element
  dice.classList.remove("rolling");
  void dice.offsetWidth;          // force reflow to restart animation
  dice.classList.add("rolling");

  setTimeout(() => {
    showFace(v);                  // update pip pattern
    bigNum.textContent = v;       // update large number
    addHistory(v);                // prepend to recent rolls strip

    // Persist to SimSense so the last roll survives a page reload
    SimSense.set("dice", "current", {
      value: v, count: c, timestamp: new Date().toISOString()
    });
  }, 620);                        // wait for animation to finish
}

Removing and re-adding the CSS class restarts the keyframe animation on each roll. Accessing offsetWidth forces the browser to recalculate layout, ensuring the animation restarts. A 620-millisecond delay allows the CSS animation to settle before updating the pip pattern.

Restoring State on Load

State restore + subscription JavaScript
(async () => {
  const data = await SimSense.get("dice", "current");
  if (data && data.value) {
    showFace(data.value);
    bigNum.textContent = data.value;
    addHistory(data.value);
    waiting.classList.add("hidden");  // hide the "waiting" overlay
  }
})();

// Real-time subscription — fires whenever SimSense state changes
SimSense.subscribe("dice", (key, value) => {
  if (key === "current") applyRoll(value);
});

SimSense.get() reads the last persisted roll immediately on page load, ensuring the dice never appears blank if a roll has previously occurred. SimSense.subscribe() provides a secondary real-time path — firing the callback whenever any key in the "dice" namespace changes.

Pip Pattern Logic

Dice face renderer JavaScript
const PATTERNS = {
  1: [5],
  2: [3, 7],
  3: [3, 5, 7],
  4: [1, 3, 7, 9],
  5: [1, 3, 5, 7, 9],
  6: [1, 3, 4, 6, 7, 9]
};

function showFace(f) {
  dice.setAttribute("data-face", f);
  for (let i = 1; i <= 9; i++)
    document.getElementById("p" + i)
      .classList.toggle("active", PATTERNS[f].includes(i));
}

The dice face is rendered as a 3×3 CSS grid with 9 cells numbered 1–9. The PATTERNS object maps each face value to the pip positions that should be active. classList.toggle() adds or removes the "active" class based on whether the position is in the pattern.

Two real-time paths

The simulator uses two independent real-time channels: MQTT delivers Arduino messages in under 100 milliseconds, while SimSense.set() persists results durably. When a second browser tab opens the simulator, SimSense.get() retrieves the last roll instantly without waiting for another button press. MQTT provides speed; SimSense provides persistence.