Using Claude Code AI to program an ESP32

This article Using Claude AI to Code an Energy Monitor to run on an ESP32 using Arduino covers how to set up Claude Code (beta) in a Window 10 machine and reports on the experience of using it to code parts of an Energy Monitor running on an ESP32.

The conclusion is
"Claude is like an enthusiastic intern and is very helpful in generating code to do the tasks you request. However Claude almost never produces error free code. Either there are small compile errors or logical errors, sometimes major ones.
Also sometimes Claude will make changes (that you did not ask for) the breaks existing working code, so ask Claude to tell you what it plans to do first."

Tips for Effective AI Coding

Don't trust Claude's code!! Check and test it carefully as you would with the code submitted by any intern.

Leave Claude's terminal session open (costs more??) until the request change is check as correct, so that when errors need to be corrected Claude already has the context of the changes requested and made.

Make the request as precise as possible with any extra information known to better guide Claude to the solution you want.

Ask Claude to tell you what it is planning to do before it does it, because is, sometimes, Claude over enthusiastic and can muck up existing code that already works.

Keep all the code related to an object / data / functionality in the same file so that when you ask Claude to change one part, Claude can see that other parts need to updated to match at the same time. Claude will do these other updates automagically.

Know when to stop making changes! Just because it is easy to add a feature does not mean you should add it. Keep it simple. Only add what is actually needed.

So true

I would add this:

My main question is, did using Claude actually save time?

Can you write the same code yourself, without Claude? - if so, then maybe AI will help you.

But If you want to use AI because you don't know how to write the code yourself - then you will most likely spend more time finding and fixing errors than if you wrote the code yourself.

I am still using it.
If I have an idea for a method, I let Claude have go at it and see what comes out.

It is good a tedious refactoring.

For example handling variables in a webpage without requiring the AsyncWebServer library. That is taking an AsyncWebServer web page with %..% variables and pulling them all out and putting them at the top of the file as script variables.

Then pulling that section out as a String so that the simple non-Async processor Claude wrote (processVars) can replace the variables in that small string using an Async type processor.

Then the basic ESP32 webserver can send that 'head' String with the variables replaced followed by the 'tail' of the rest on the constant html file using this (hand coded) method

static void handleIndex() {
  String newHeader = processVars(index_head);
  sendHeaderAndTail(newHeader, "/index.tail");
}
static void sendHeaderAndTail(String & header, const char*tailPath) {
  if (debugPtr) {
    debugPtr->print(" sendHeaderAndTail.  tail File: "); debugPtr->println(tailPath);
    debugPtr->println(" header:-------- "); debugPtr->println(header);
    debugPtr->println(" ======= ");
  }
  File dataFile = LittleFS.open(tailPath);

  if (!dataFile) {
    Serial.print(" Failed to open:"); Serial.println(tailPath);
    return;
  }

  size_t fileSize = dataFile.size();
  NetworkClient currentClient = server.client();
  server.setContentLength(fileSize + header.length());
  String contentType = "text/html";
  const int code = 200;
  server.send(code, contentType, "");
  size_t headerSent = currentClient.write(header.c_str(), header.length());
  (void)(headerSent);
  size_t dataSent = currentClient.write(dataFile);
  if (dataSent != dataFile.size()) {
    if (debugPtr) {
      debugPtr->print(" Sent less data than expected from file: ");    debugPtr->println(tailPath);
    }
  }
  dataFile.close();
}

Update: 2nd April 2025 –
Claude Code's code returned a number of infinite loops, which when pointed out to Claude, Claude could not fix.

Claude just went round and round changing logic tests but not removing the loop.
Only after being instructed to introduce another state variable was Claude able to break the loop.

Let me revise the comment. I no longer trust claude to do refactoring.
It often mucks things up.

I still use claude on a regular basis, but for very local changes / enhancements or for generating a general overview of the the code base.

claude is the main author of the detailed code in the new pfodWeb and pfodWebDesigner projects, which given my lack of experience with javascript, would not have been practical without claude’s assistance

Just finished using Claude AI to write the interface code between a sensor, C4001 mmWave Human Detection, and a User Interface GUI designed by the free pfodWebDesigner.

See AI coded Arduino interface to Sensor pfodWeb/pfodWebDesigner GUI for the single prompt used.

The Ai coding cost less than 50 cents and the resulting code was almost perfect.

The C4001 has 9 tuning adjustments that effect it operation. The GUI gives you access to those via Serial, or BLE or HTTP.

Using a similar apporach you can quickly create an interface for most sensors.

Using claud.ai to create Arduino code

I never thought I’d use an AI for coding, that said here is a plug and play bit of Claude code. Nice and neat I’ve found it’s a great starting place even if it needs tweaking.

Amazingly it (almost) worked first time !

To be fair I haven't connect the relay drivers yet, seems it needs a wee tweak there.

{"solar_controller": 1, "inverter_power": 0, "boost_charger": 1}

Data in MQTT

Actual data in node red

No wonder I got made redundant :thinking:

Arduino Nano - Data Acquisition and Control System

The prompt

I want a data acquisition system based on an Arduino nano with the following inputs battery voltage, solar charge current, boost charge current, Mains voltage which will come from an analogue device as a voltage. I also want athe following digital outputs. Solar charge controller on, inverter power on, boost charger on. The output data should be packaged as json and sent via the serial port to a esp8266 which will act as the network adaptor and send the data via Mqtt. Command's will be sent to the esp8266 using Mqtt and then sent via serial to The Arduino nano which will need to catch this data and act on it switching outputs etc. all data will be sent as raw bits in the json packets. I need sketches for both the esp8266 and the arduino nano

Claude said

I'll create comprehensive sketches for both the Arduino Nano (data acquisition and control) and the ESP8266 (network adapter with MQTT).

Arduino Nano - Data Acquisition System

Code

/* * Arduino Nano - Data Acquisition and Control System * Reads analog inputs, controls digital outputs * Communicates via JSON over Serial with ESP8266 */ #include <ArduinoJson.h> // Analog Input Pins const int PIN_BATTERY_VOLTAGE = A0; const

ESP8266 - MQTT Network Adapter

Code

/* * ESP8266 - MQTT Network Adapter * Bridges serial communication from Arduino Nano to MQTT * Receives commands via MQTT and forwards to Arduino Nano */ #include <ESP8266WiFi.h> #include <PubSubClient.h> #include <ArduinoJson.h> // WiFi creden

I've created both sketches for your data acquisition system. Here's what each does:

Arduino Nano Sketch

Features:

  • Reads 4 analog inputs (battery voltage, solar current, boost current, mains voltage) as raw ADC values (0-1023)

  • Controls 3 digital outputs (solar controller, inverter power, boost charger)

  • Sends data as JSON every second via serial

  • Receives commands via serial and controls outputs accordingly

  • Includes error handling and acknowledgments

ESP8266 Sketch

Features:

  • Connects to WiFi and MQTT broker

  • Forwards data from Arduino to MQTT topic daq/data

  • Subscribes to daq/command topic for control commands

  • Publishes status messages to daq/status topic

  • Validates JSON before forwarding

  • Auto-reconnects to WiFi and MQTT if connection lost

Setup Instructions:

1. Install Required Libraries:

  • Arduino Nano: ArduinoJson (install via Library Manager)

  • ESP8266: ESP8266WiFi, PubSubClient, ArduinoJson

2. Configure ESP8266:

  • Update WiFi credentials (WIFI_SSID, WIFI_PASSWORD)

  • Update MQTT broker settings (MQTT_BROKER, MQTT_PORT)

  • Add username/password if your broker requires authentication

3. Wiring:

  • Connect Arduino TX to ESP8266 RX

  • Connect Arduino RX to ESP8266 TX

  • Share common ground between devices

4. MQTT Command Format:

json

{"solar_controller": 1, "inverter_power": 0, "boost_charger": 1}

5. Data Output Format:

json

{
  "battery_voltage": 512,
  "solar_current": 256,
  "boost_current": 128,
  "mains_voltage": 768,
  "solar_controller": 1,
  "inverter_power": 0,
  "boost_charger": 1,
  "timestamp": 12345
}

The system sends raw ADC bits as you requested. To convert to actual voltage: voltage = (raw_value * 5.0) / 1023.0

/*
 * Arduino Nano - Data Acquisition and Control System
 * Reads analog inputs, controls digital outputs
 * Communicates via JSON over Serial with ESP8266
 */

#include <ArduinoJson.h>

// Analog Input Pins
const int PIN_BATTERY_VOLTAGE = A0;
const int PIN_SOLAR_CURRENT = A1;
const int PIN_BOOST_CURRENT = A2;
const int PIN_MAINS_VOLTAGE = A3;

// Digital Output Pins
const int PIN_SOLAR_CONTROLLER = 2;
const int PIN_INVERTER_POWER = 3;
const int PIN_BOOST_CHARGER = 4;

// Timing
unsigned long lastSendTime = 0;
const unsigned long SEND_INTERVAL = 1000; // Send data every 1 second

// ADC to voltage conversion (Arduino Nano: 5V reference, 10-bit ADC)
const float ADC_REF_VOLTAGE = 5.0;
const int ADC_RESOLUTION = 1023;

void setup() {
  // Initialize Serial communication with ESP8266
  Serial.begin(115200);
  
  // Configure analog input pins
  pinMode(PIN_BATTERY_VOLTAGE, INPUT);
  pinMode(PIN_SOLAR_CURRENT, INPUT);
  pinMode(PIN_BOOST_CURRENT, INPUT);
  pinMode(PIN_MAINS_VOLTAGE, INPUT);
  
  // Configure digital output pins
  pinMode(PIN_SOLAR_CONTROLLER, OUTPUT);
  pinMode(PIN_INVERTER_POWER, OUTPUT);
  pinMode(PIN_BOOST_CHARGER, OUTPUT);
  
  // Initialize outputs to OFF
  digitalWrite(PIN_SOLAR_CONTROLLER, LOW);
  digitalWrite(PIN_INVERTER_POWER, LOW);
  digitalWrite(PIN_BOOST_CHARGER, LOW);
  
  // Wait for serial to initialize
  delay(100);
}

void loop() {
  // Check for incoming commands from ESP8266
  if (Serial.available() > 0) {
    processCommand();
  }
  
  // Send data at regular intervals
  unsigned long currentTime = millis();
  if (currentTime - lastSendTime >= SEND_INTERVAL) {
    sendData();
    lastSendTime = currentTime;
  }
}

void sendData() {
  // Read analog inputs (raw ADC values 0-1023)
  int batteryVoltageRaw = analogRead(PIN_BATTERY_VOLTAGE);
  int solarCurrentRaw = analogRead(PIN_SOLAR_CURRENT);
  int boostCurrentRaw = analogRead(PIN_BOOST_CURRENT);
  int mainsVoltageRaw = analogRead(PIN_MAINS_VOLTAGE);
  
  // Read digital output states
  int solarControllerState = digitalRead(PIN_SOLAR_CONTROLLER);
  int inverterPowerState = digitalRead(PIN_INVERTER_POWER);
  int boostChargerState = digitalRead(PIN_BOOST_CHARGER);
  
  // Create JSON document
  StaticJsonDocument<256> doc;
  
  // Add analog readings (raw ADC values)
  doc["battery_voltage"] = batteryVoltageRaw;
  doc["solar_current"] = solarCurrentRaw;
  doc["boost_current"] = boostCurrentRaw;
  doc["mains_voltage"] = mainsVoltageRaw;
  
  // Add digital output states
  doc["solar_controller"] = solarControllerState;
  doc["inverter_power"] = inverterPowerState;
  doc["boost_charger"] = boostChargerState;
  
  // Add timestamp
  doc["timestamp"] = millis();
  
  // Serialize and send
  serializeJson(doc, Serial);
  Serial.println(); // Add newline for message delimiter
}

void processCommand() {
  // Read JSON command from serial
  String jsonString = Serial.readStringUntil('\n');
  
  // Parse JSON
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, jsonString);
  
  if (error) {
    // Send error response
    StaticJsonDocument<100> errorDoc;
    errorDoc["error"] = "JSON parse failed";
    errorDoc["code"] = error.c_str();
    serializeJson(errorDoc, Serial);
    Serial.println();
    return;
  }
  
  // Process commands
  bool changed = false;
  
  if (doc.containsKey("solar_controller")) {
    int value = doc["solar_controller"];
    digitalWrite(PIN_SOLAR_CONTROLLER, value ? HIGH : LOW);
    changed = true;
  }
  
  if (doc.containsKey("inverter_power")) {
    int value = doc["inverter_power"];
    digitalWrite(PIN_INVERTER_POWER, value ? HIGH : LOW);
    changed = true;
  }
  
  if (doc.containsKey("boost_charger")) {
    int value = doc["boost_charger"];
    digitalWrite(PIN_BOOST_CHARGER, value ? HIGH : LOW);
    changed = true;
  }
  
  // Send acknowledgment
  if (changed) {
    StaticJsonDocument<100> ackDoc;
    ackDoc["status"] = "ok";
    ackDoc["message"] = "Command executed";
    serializeJson(ackDoc, Serial);
    Serial.println();
  }
}
/*
 * ESP8266 - MQTT Network Adapter
 * Bridges serial communication from Arduino Nano to MQTT
 * Receives commands via MQTT and forwards to Arduino Nano
 */

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

// WiFi credentials
const char* WIFI_SSID = "YourWiFiSSID";
const char* WIFI_PASSWORD = "YourWiFiPassword";

// MQTT Broker settings
const char* MQTT_BROKER = "mqtt.example.com"; // Change to your broker
const int MQTT_PORT = 1883;
const char* MQTT_CLIENT_ID = "ESP8266_DAQ";
const char* MQTT_USERNAME = ""; // Leave empty if not required
const char* MQTT_PASSWORD = ""; // Leave empty if not required

// MQTT Topics
const char* TOPIC_DATA = "daq/data";           // Publishes sensor data
const char* TOPIC_COMMAND = "daq/command";     // Subscribes to commands
const char* TOPIC_STATUS = "daq/status";       // Publishes system status

WiFiClient espClient;
PubSubClient mqttClient(espClient);

// Timing
unsigned long lastReconnectAttempt = 0;
const unsigned long RECONNECT_INTERVAL = 5000;

void setup() {
  // Initialize serial communication with Arduino Nano
  Serial.begin(115200);
  
  // Connect to WiFi
  setupWiFi();
  
  // Configure MQTT
  mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
  mqttClient.setCallback(mqttCallback);
  mqttClient.setBufferSize(512); // Increase buffer if needed
}

void loop() {
  // Maintain MQTT connection
  if (!mqttClient.connected()) {
    unsigned long now = millis();
    if (now - lastReconnectAttempt > RECONNECT_INTERVAL) {
      lastReconnectAttempt = now;
      if (reconnectMQTT()) {
        lastReconnectAttempt = 0;
      }
    }
  } else {
    mqttClient.loop();
  }
  
  // Check for data from Arduino Nano
  if (Serial.available() > 0) {
    processArduinoData();
  }
}

void setupWiFi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to WiFi: ");
  Serial.println(WIFI_SSID);
  
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println();
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println();
    Serial.println("WiFi connection failed");
  }
}

boolean reconnectMQTT() {
  Serial.print("Attempting MQTT connection...");
  
  boolean connected;
  if (strlen(MQTT_USERNAME) > 0) {
    connected = mqttClient.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD);
  } else {
    connected = mqttClient.connect(MQTT_CLIENT_ID);
  }
  
  if (connected) {
    Serial.println("connected");
    
    // Subscribe to command topic
    mqttClient.subscribe(TOPIC_COMMAND);
    
    // Publish status
    StaticJsonDocument<100> statusDoc;
    statusDoc["status"] = "online";
    statusDoc["client"] = MQTT_CLIENT_ID;
    String statusJson;
    serializeJson(statusDoc, statusJson);
    mqttClient.publish(TOPIC_STATUS, statusJson.c_str(), true); // Retained message
    
  } else {
    Serial.print("failed, rc=");
    Serial.print(mqttClient.state());
    Serial.println(" retrying in 5 seconds");
  }
  
  return connected;
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
  // Convert payload to string
  String message;
  for (unsigned int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  
  Serial.print("MQTT message received on topic: ");
  Serial.println(topic);
  Serial.print("Message: ");
  Serial.println(message);
  
  // If it's a command, forward to Arduino Nano
  if (strcmp(topic, TOPIC_COMMAND) == 0) {
    forwardCommandToArduino(message);
  }
}

void forwardCommandToArduino(String command) {
  // Validate JSON before forwarding
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, command);
  
  if (error) {
    Serial.println("Invalid JSON command, not forwarding");
    
    // Publish error to MQTT
    StaticJsonDocument<100> errorDoc;
    errorDoc["error"] = "Invalid JSON in command";
    String errorJson;
    serializeJson(errorDoc, errorJson);
    mqttClient.publish(TOPIC_STATUS, errorJson.c_str());
    return;
  }
  
  // Forward to Arduino Nano
  Serial.println(command);
  Serial.println(); // Ensure newline delimiter
}

void processArduinoData() {
  // Read line from Arduino
  String jsonString = Serial.readStringUntil('\n');
  
  if (jsonString.length() == 0) {
    return;
  }
  
  // Validate JSON
  StaticJsonDocument<512> doc;
  DeserializationError error = deserializeJson(doc, jsonString);
  
  if (error) {
    Serial.print("Failed to parse JSON from Arduino: ");
    Serial.println(error.c_str());
    return;
  }
  
  // Check if it's an error/ack message or data
  if (doc.containsKey("error") || doc.containsKey("status")) {
    // Publish to status topic
    String outputJson;
    serializeJson(doc, outputJson);
    mqttClient.publish(TOPIC_STATUS, outputJson.c_str());
  } else {
    // Publish sensor data to data topic
    String outputJson;
    serializeJson(doc, outputJson);
    mqttClient.publish(TOPIC_DATA, outputJson.c_str());
  }
}

Since the Arduino Nano is a 5 volt device and the ESP8266 a 3.3 volt device the normal practice is to use a level shifter here. See what it comes back with when you challenge it with my comment.
Also provide a schematic diagram to accompany your code.