Monday, 13 April 2026

Using Visual Studio Code with the ESP-IDE extension and Claude Code

This is a bit of a step for me, having not done any half serious coding for may years. Given the atrophied state of my aged brain, this won't be easy. However, Claude Code has come to the rescue....

To install this collection you need to:

  • Install Visual Studio Code, aka VSC (already present, as I've been using this for a variety of applications, such as Javascript for post processor modification, LinuxCNC HAL file development, CNC file editing etc).
  • Install Espressif's ESP-IDF, which is their IDE for development of code for their ESP micrcontroller family.
  • Install the ESP-IDF extension for VSC. On the face of it, this should be possible (easy?) from within VSC but I tried and failed a few times. Apparently this is not unusual (being a MS product and all), so installing IDF first seems to be the workaround.
  • Also, ideally install Microsoft's C/C++ development tools, debugger etc extensions for VSC (install from within VSC).
  • Then, set them up and get them working (arguably shouldn't be a big task but....). I used the "Hello world" example as the simplest possible instance for testing.
  • And of course, you need to install Claude Code from Anthropic. If you want Claude to create code for you, you will need the Individual / Pro subscription option which costs £15/mo. It's cancellable at any time.
And of course, you now need to be using full-blown C, rather than the higher level, simplified C++ that Arduino is based on. But that's where Claude Code (CC) comes in.

Copying the hello_world example and renaming / resaving it is the cowardly way to get started with VSC/IDF once it's actually up and running. Then you can get Claude busy coding for you:


The little symbols on the taskbar at the bottom allow you to configure the ESP-IDF for your specific ESP32 board (ESP32-S3 in my case), select the COM port, compile, build and flash the code and start the output monitor.

I'd equate the competence of Claude with that of a fairy enthusiastic and excitable but far from infallible senior software engineer - very much like your archetypal American (Microsoft?) cliche.


I find I have to get Claude to thrown the code at the wall a few times before it will stick. But having said that, to extend that metaphor, I'd struggle even to come up with anything to throw in the first place.

What about that test jig you promised, Fatty?

Well, in order to test out the software, I need a physical setup that will allow the Bojke laser distance sensor and the load cell / HX711 to send data simultaneously to the ESP32. Trying to do this by hand merely results in the graph producing horizontal or vertical lines (if twiddling one or other of the sensors) or just a load of random bollocks if I twiddle them at the same time.

What I need is a spring element that generates both a force at the load cell and a displacement at the distance sensor. Ideally I'd also be able to generate some degree of hysteresis (lost motion) but that might be a step for later. To get up and running, I've created the following assembly:



Forgive the auto-generated colours - they allow you to see the different elements within the assembly. It's possible the section view will add some clarity:


It's an odd looking thing but it has to accommodate several elements:
  • the awkwardly shaped load cell (yellow, bottom) 
  • the laser sensor (shell body at the top right).
  • the spring element (which is actually a 10cm steel ruler)
  • a sliding block that allows physical limitation of the movement of the spring. 
This concept doesn't actually provide hysteresis so much as a 2-stage spring stiffness factor, but it's sort of close enough for now. I can adjust the position of the block along the spring and also limit the vertical movement of the block before it. There are 3 grub screws in the block. One locks it in position along the spring, while the other 2 provide adjustable limits on the vertical displacement of the block. Thereafter, with any further movement of the end of the spring, the effective spring rate is increased.

And indeed, here it is, hot off the 3D printer:


Operation is simple and crude:
  • Start the web server by powering up the ESP32 and opening a browser that points to the hard coded IP address.
  • The load cell is tared at the initial setting and the displacement measurement is zeroed at its rest position.
  • Start the graphing function (clear the screen first, if necessary) and press on the end of the spring. To characterise the opposite direction, lift the end of the spring.
  • Stop the graphing function, to prevent further readings.
Here's the output of the first iteration of my stiffness gauge code, which generates a web page with the acquired data:


That works. Next steps:
  • Linear regression on loading and unloading curves separately for accurate stiffness calculation
  • Automatic reversal detection to split the two curves
  • Backlash calculation from the hysteresis gap
It's not intended to be some finely honed device at this stage. But I'm exploring how useful (competent) Claude Code is as much as anything. But that will do for the moment......

Monday, 6 April 2026

Logic analyser LA1010

Before completely giving up on the Arduino IDE, I got one of the Saleae clone logic analysers. These use an Altera Cyclone FPGA to capture up to 16 channels of digital inputs, then send the data to a Cypress FX2 microcontroller and thence over USB to your PC. From there, there's some rather powerful open source software for processing, displaying and decoding the data. Many different protocols are supported, not least Modbus of course.

Here's what you get on a scope. Very interesting - but you can't tell what is and isn't being sent over the bus. For instance, is it missing any critical bits? Does the CRC check agree. What are we looking at?


You can watch the bus using a tool like qModMaster or Hercules but they won't tell you much if the message is corrupted or missing.


This is where these cheap logic analaysers come in. Mine's from Kingst and 
https://qdkingst.com/en


Schematic (reverse engineered) https://sigrok.org/wimg/2/26/Kingst_LA2016_LA1016_Schematic.zip

What's inside:

This very handy and insightful video overview on the OpenTechLab channel was created by the originator of the sigrok project:


The software is free and very powerful:

But enough of the gadgets. It's time to pull on those Big Boy Pants and start using grown up coding tools.....

Saturday, 4 April 2026

Arduino + ESP32 + RS485 = nightmare

What's wrong now, Fatty?

Last time I used the ESP32-S3-POE-ETH-8DI-8DO module, it was happy to talk to the Bojke laser sensor. But can I get the ESP32-S3 Mini to talk to it? Can I f*ck.

Claude Code got its knickers in a good old twist trying to figure it out but finally I lost the will to live.

Here's what I had:

/**
 * ============================================================
 *  BOJKE BL-30NZ-485 Laser Displacement Sensor
 *  Waveshare ESP32-S3-POE-ETH-8DI-8DO board
 * ============================================================
 */
#include <Arduino.h>
#include <Wire.h>
// ── I2C pins (from I2C_Driver.h) ─────────────────────────────
#define I2C_SCL_PIN   41
#define I2C_SDA_PIN   42
// ── TCA9554 I2C expander (from WS_TCA9554PWR.h) ──────────────
#define TCA9554_ADDRESS     0x20
#define TCA9554_INPUT_REG   0x00
#define TCA9554_OUTPUT_REG  0x01
#define TCA9554_CONFIG_REG  0x03
// ── RS485 pins (from WS_GPIO.h) ──────────────────────────────
#define TXD1     17
#define RXD1     18
#define TXD1EN   21
// ── RS485 serial port ─────────────────────────────────────────
HardwareSerial rs485Serial(1);
// ── Modbus settings ───────────────────────────────────────────
#define MODBUS_BAUD                115200
#define MODBUS_SLAVE_ID            0x01
#define SENSOR_REG_ADDR            0x0000
#define SENSOR_REG_COUNT           0x0002
#define MODBUS_RESPONSE_TIMEOUT_MS 500
#define EXPECTED_RESPONSE_LEN      9
#define POLL_INTERVAL_MS           1000
// ─────────────────────────────────────────────────────────────
//  TCA9554 helpers
// ─────────────────────────────────────────────────────────────
void TCA9554_WriteReg(uint8_t reg, uint8_t data) {
    Wire.beginTransmission(TCA9554_ADDRESS);
    Wire.write(reg);
    Wire.write(data);
    Wire.endTransmission();
}
void TCA9554_Init(uint8_t pinMode, uint8_t pinState) {
    TCA9554_WriteReg(TCA9554_OUTPUT_REG, pinState);
    TCA9554_WriteReg(TCA9554_CONFIG_REG, pinMode);
}
// ─────────────────────────────────────────────────────────────
//  CRC-16/IBM
// ─────────────────────────────────────────────────────────────
uint16_t modbusCRC(const uint8_t *buf, uint8_t len) {
    uint16_t crc = 0xFFFF;
    for (uint8_t i = 0; i < len; i++) {
        crc ^= (uint16_t)buf[i];
        for (uint8_t j = 0; j < 8; j++)
            crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
    }
    return crc;
}
// ─────────────────────────────────────────────────────────────
//  Build FC04 request
// ─────────────────────────────────────────────────────────────
void buildFC04Request(uint8_t *frame, uint8_t slaveId,
                      uint16_t regAddr, uint16_t regCount) {
    frame[0] = slaveId;
    frame[1] = 0x04;
    frame[2] = regAddr >> 8;
    frame[3] = regAddr & 0xFF;
    frame[4] = regCount >> 8;
    frame[5] = regCount & 0xFF;
    uint16_t crc = modbusCRC(frame, 6);
    frame[6] = crc & 0xFF;
    frame[7] = crc >> 8;
}
// ─────────────────────────────────────────────────────────────
//  Read sensor distance
// ─────────────────────────────────────────────────────────────
float readDistanceMM() {
    while (rs485Serial.available()) rs485Serial.read();
    uint8_t request[8];
    buildFC04Request(request, MODBUS_SLAVE_ID,
                     SENSOR_REG_ADDR, SENSOR_REG_COUNT);
    Serial.print("[TX] ");
    for (int i = 0; i < 8; i++) {
        if (request[i] < 0x10) Serial.print('0');
        Serial.print(request[i], HEX);
        Serial.print(' ');
    }
    Serial.println();
    rs485Serial.write(request, sizeof(request));
    rs485Serial.flush();
    uint8_t response[EXPECTED_RESPONSE_LEN];
    uint8_t bytesReceived = 0;
    uint32_t deadline = millis() + MODBUS_RESPONSE_TIMEOUT_MS;
    while (millis() < deadline && bytesReceived < EXPECTED_RESPONSE_LEN) {
        if (rs485Serial.available())
            response[bytesReceived++] = rs485Serial.read();
    }
    Serial.print("[RX] ");
    for (int i = 0; i < bytesReceived; i++) {
        if (response[i] < 0x10) Serial.print('0');
        Serial.print(response[i], HEX);
        Serial.print(' ');
    }
    Serial.println();
    if (bytesReceived < EXPECTED_RESPONSE_LEN) {
        Serial.println("[ERR] Timeout / incomplete response");
        return NAN;
    }
    if (response[0] != MODBUS_SLAVE_ID) { Serial.println("[ERR] Wrong slave ID"); return NAN; }
    if (response[1] == 0x84) { Serial.println("[ERR] Modbus exception"); return NAN; }
    if (response[1] != 0x04) { Serial.println("[ERR] Wrong function code"); return NAN; }
    uint16_t rxCRC   = (uint16_t)response[7] | ((uint16_t)response[8] << 8);
    uint16_t calcCRC = modbusCRC(response, 7);
    if (rxCRC != calcCRC) { Serial.println("[ERR] CRC mismatch"); return NAN; }
    uint16_t raw = ((uint16_t)response[5] << 8) | response[6];
    return raw * 0.001f;
}
// ─────────────────────────────────────────────────────────────
//  setup()
// ─────────────────────────────────────────────────────────────
void setup() {
    Serial.begin(115200);
    delay(2000);
    Serial.println("\n=== BOJKE BL-30NZ-485 on Waveshare ESP32-S3-POE-ETH-8DI-8DO ===");
    // I2C init
    Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
    Serial.println("I2C initialised");
    // TCA9554 init — all outputs, all HIGH (matches Waveshare Dout_Init)
    TCA9554_Init(0x00, 0xFF);
    Serial.println("TCA9554 initialised");
    // RS485 init — exactly as Waveshare demo
    rs485Serial.begin(MODBUS_BAUD, SERIAL_8N1, RXD1, TXD1);
    if (!rs485Serial.setPins(-1, -1, -1, TXD1EN))
        Serial.println("[WARN] Failed to set TXDEN pin");
    if (!rs485Serial.setMode(UART_MODE_RS485_HALF_DUPLEX))
        Serial.println("[WARN] Failed to set RS485 half-duplex mode");
    Serial.println("RS485 initialised");
    Serial.println("================================================================\n");
}
// ─────────────────────────────────────────────────────────────
//  loop()
// ─────────────────────────────────────────────────────────────
void loop() {
    float distance = readDistanceMM();
    if (!std::isnan(distance)) {
        if (distance >= 25.0f && distance <= 35.0f) {
            Serial.print("Distance: ");
            Serial.print(distance, 3);
            Serial.println(" mm");
        } else {
            Serial.println("Distance: OUT OF RANGE");
        }
    } else {
        Serial.println("Distance: READ ERROR");
    }
    Serial.println();
    delay(POLL_INTERVAL_MS);
}

And here's the proof that when I used a USB-RS485 dongle to sniff the traffic, it was working fine:


...using these port settings:


So there's nothing wrong with the Bojke sensor. The Stupid Fat Bloke hasn't been messing about with it behind my back and blown it up.

To get up to speed with Modbus, here's a handy little intro video:


I was starting to get the feeling there was something going on here that nobody told me about. But with some digging, I found this vid discussing why Modbus doesn't work with ESP32:


Bottom line is, Arduino is a sort of Noddy / Newbie IDE for developing code to program the Arduino family and as such it is a fairly high level, cut down version of C++, where many of the functions are actually hidden from the user, to keep life "simple"(!!). Furthermore, although the ESP32 has a lot of support within the Arduino IDE, it's pushing the capabilities of some of the functions that were never intended to be used beyond the actual Arduino family. One of those might be the support for implementing RS485.

Here's an interesting post on LinkedIn (assuming access isn't limited:

Why does the ESP32 Arduino core struggle with RS485 software flow control? 🧐
​I recently ran a series of RS485 communication tests using the MAX485 module. Most configurations worked exactly as expected—until I hit a specific wall.
​I successfully tested:
🔹 STM32 on both CubeIDE and Arduino (Soft Flow Control).
🔹 ESP32 using Espressif Systems ESP-IDF (Soft Flow Control).
🔹 ESP32 using Arduino (Hardware Flow Control).
​The catch?
When trying to implement Software Flow Control on the ESP32 using the Arduino framework, the communication failed.
​Since it works fine on ESP-IDF, I suspect it’s an overhead or timing issue within the Arduino core implementation for the ESP32, specifically regarding when the DE/RE pins are toggled relative to the UART buffer.
​Has anyone patched this or found a workaround? Let’s discuss in the comments! 

This reply was pretty insightful:

You had me confused there for some time because RS485 has no notion of either hardware or software flow control like RS232 does, but
I infer that you are reffering to the RS485 control of direction . A software command to assert the transmitter enable gpio pin manually works .
It is possible to assert the control line before starting a transmission , and negating it after transamission . I myself have done that often .
However , the problem almost always is the timing of the negation. It is not good enough to negate the TE/RE control line when the transmit buffers
and FIFO becomes empty , as the last byte are still in the transmit shift register being shifted out , and a premature negation of the TE/RE
will cause lost of transmitted data . To overcome this , you will have to spin at the end of transmission waiting for
the transmit shift register to also become empty , and then negate the RE/TE line.
You can use use uart_wait_tx_done(.........) function to block until all bits have been shifted.

So using the Arduino IDE to program an ESP32 to use RS485 may be a bit of an oxymoron - and nobody's going to act surprised or run to help you if you run into problems. The grownups will be using "proper" IDEs such as the dedicated ESP-IDF that Espressif have developed specifically to ..... program the ESP32 family. Oh well, time to "put my big boy pants on" and move on from this time consuming fiasco....

Saturday, 21 March 2026

Getting an ESP32-S3-Mini to talk to an HX711 load cell amplifier and the Bojke laser distance sensor

It's all very well using that large DIN Rail-mounted ESP32-S3-POE-ETH-8DI-8DO to talk to the laser sensor and load cell but it feels a bit over the top. There's also a problem with this product when you want to connect to an HX711. This device uses I2C to connect up its 8 digital outputs, rather than go directly from the ESP32's GPIO. The HX711 uses a proprietary 2 wire serial protocol (not I2C or SPI), just clock and data, so requires "proper" GPIO digital outputs to work.

How about simply running both the HX711 and the Bojke sensor on one device, namely the ESP32-S3-Mini? I have a couple of these (and some other flavours eg RP2040) that I acquired for the MIG torch driver ("Motorhead"). That would allow me to drive the HX711 using the GPIO and talk to the Bojke sensor over RS485.

Rabbit hole warning - Claude Code:

I asked Claude Code to draw it up as a simple schematic but TBH it's not very good at that kind of task. To do so, it actually created a Python sketch that it then ran to create this graphic. Took a few minutes too:


I said I wasn't quite there with its effort, so I asked it to come up with a table in JPG format. I suppose it's a bit better in some respects:


Hmm, that's better but I'd rather work from some sort of schematic when it comes to wiring the thing up. So it then came up with this:

...and this:


Not sure that's much of an improvement from where I stand. It offered me the schematic in Eagle (Fusion 360), so in the interests of science I then gave that a go. That caused my GMKtec NUCbox9 to hang and stutter badly (a known issue with a recent Windows "update" and the on-chip Ryzen graphics) and the resulting schematic was pretty shit. I was only able to grab this screenshot by opening the schematic on my XPS15 (which has Nvidia graphics, so wasn't stricken by the same Windows conflict):


It suggested replacing the blocks with pukka Eagle symbols but that turned out to be a fool's errand that finally finished me off - talk about shit results! The original block diagram is the best I can get from it, alongside the connection table as a tabular version.

Stop buggering about, Fatty:

After that rather fruitless diversion, perhaps it's time to get on with wiring the thing up. I think I'll use a breadboard for this to start with, otherwise I'll end up with several boards all hanging together with a spider's web of wiring.

Bojack to the rescue! No idea when I got this one but it saves stripping out the other breadboards I used for the Motorhead.


So without much more pointless buggerage, let's try and lash this thing up, using the table and block schematic.

Before I forget, here's the Arduino code for the combined sensors:
It seems to compile and upload to the ESP32-S3-Mini without issues - but of course it won't do anything without the HX711 and Bojke sensor connected up.

/**
 * ============================================================
 *  Waveshare ESP32-S3-Zero
 *  HX711 load cell + BOJKE BL-30NZ-485 laser — stiffness logger
 * ============================================================
 *
 *  WIRING
 *  ──────
 *  HX711
 *    VCC  → 3.3V
 *    GND  → GND
 *    DT   → GPIO2
 *    SCK  → GPIO3
 *    RATE → 3.3V for 80Hz, GND/float for 10Hz
 *
 *  MAX485 module
 *    VCC  → 5V
 *    GND  → GND
 *    DI   → GPIO1  (UART1 TX)
 *    RO   → GPIO0  (UART1 RX)
 *    DE+RE tied together → GPIO4
 *    A/B screw terminals → BOJKE green(A+) / white(B-)
 *
 *  BOJKE BL-30NZ-485
 *    brown  → +12-24V DC
 *    blue   → GND
 *    green  → A(+) on MAX485
 *    white  → B(-) on MAX485
 *
 *  CALIBRATION
 *  ───────────
 *  Set CALIBRATION_FACTOR to 0.0 on first flash.
 *  Follow Serial Monitor prompts (115200 baud).
 *  Paste the printed factor back in and re-flash.
 *
 *  CSV OUTPUT (USB Serial, 115200)
 *  ───────────────────────────────
 *  # timestamp_ms, distance_mm, force_g
 *  1000, 28.450, 0.0
 *  1050, 28.451, 12.3
 *  ...
 *
 *  POLL RATE
 *  ─────────
 *  POLL_INTERVAL_MS 50  → 20 Hz (safe default)
 *  POLL_INTERVAL_MS 12  → ~80 Hz (requires RATE pin HIGH on HX711)
 * ============================================================
 */

#include <Arduino.h>
#include <HX711.h>

// ── HX711 ────────────────────────────────────────────────────
#define HX711_DT_PIN    2
#define HX711_SCK_PIN   3
#define HX711_READINGS  3     // averages per sample (lower = faster)

// ── MAX485 / RS485 ───────────────────────────────────────────
#define RS485_TX_PIN    1
#define RS485_RX_PIN    0
#define RS485_DE_PIN    4

// ── BOJKE Modbus settings ────────────────────────────────────
#define MODBUS_BAUD              115200
#define LASER_SLAVE_ID           0x01
#define LASER_REG_ADDR           0x0000
#define LASER_REG_COUNT          0x0002
#define LASER_RESPONSE_TIMEOUT   50    // ms
#define LASER_EXPECTED_LEN       9     // bytes

// ── Poll rate ─────────────────────────────────────────────────
#define POLL_INTERVAL_MS   50

// ── Calibration ──────────────────────────────────────────────
// Set to 0.0 to run calibration on boot.
// After calibration paste the printed value here and re-flash.
#define CALIBRATION_FACTOR   0.0f

// Known weight used during calibration (grams)
#define CALIBRATION_WEIGHT_G  500.0f

// ─────────────────────────────────────────────────────────────

HX711 scale;
HardwareSerial rs485(1);

// ─────────────────────────────────────────────────────────────
//  CRC-16/IBM (Modbus)
// ─────────────────────────────────────────────────────────────
uint16_t modbusCRC(const uint8_t *buf, uint8_t len) {
    uint16_t crc = 0xFFFF;
    for (uint8_t i = 0; i < len; i++) {
        crc ^= (uint16_t)buf[i];
        for (uint8_t j = 0; j < 8; j++)
            crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
    }
    return crc;
}

// ─────────────────────────────────────────────────────────────
//  Read laser distance (mm) via Modbus RTU FC04
// ─────────────────────────────────────────────────────────────
float readLaserMM() {
    // Flush any stale bytes
    while (rs485.available()) rs485.read();

    // Build FC04 request
    uint8_t req[8];
    req[0] = LASER_SLAVE_ID;
    req[1] = 0x04;
    req[2] = LASER_REG_ADDR >> 8;
    req[3] = LASER_REG_ADDR & 0xFF;
    req[4] = LASER_REG_COUNT >> 8;
    req[5] = LASER_REG_COUNT & 0xFF;
    uint16_t crc = modbusCRC(req, 6);
    req[6] = crc & 0xFF;
    req[7] = crc >> 8;

    digitalWrite(RS485_DE_PIN, HIGH);
    delayMicroseconds(100);
    rs485.write(req, sizeof(req));
    rs485.flush();
    delayMicroseconds(100);
    digitalWrite(RS485_DE_PIN, LOW);

    // Wait for response
    uint8_t resp[LASER_EXPECTED_LEN];
    uint8_t n = 0;
    uint32_t deadline = millis() + LASER_RESPONSE_TIMEOUT;
    while (millis() < deadline && n < LASER_EXPECTED_LEN) {
        if (rs485.available())
            resp[n++] = rs485.read();
    }

    if (n < LASER_EXPECTED_LEN)                   return NAN;
    if (resp[0] != LASER_SLAVE_ID)                return NAN;
    if (resp[1] != 0x04)                          return NAN;

    uint16_t rxCRC   = (uint16_t)resp[7] | ((uint16_t)resp[8] << 8);
    uint16_t calcCRC = modbusCRC(resp, 7);
    if (rxCRC != calcCRC)                         return NAN;

    uint16_t raw = ((uint16_t)resp[3] << 8) | resp[4];
    return raw * 0.001f;
}

// ─────────────────────────────────────────────────────────────
//  Calibration routine
// ─────────────────────────────────────────────────────────────
void runCalibration() {
    Serial.println("\n========================================");
    Serial.println("  HX711 CALIBRATION");
    Serial.println("========================================");
    Serial.println("Step 1: Remove ALL load from the cell.");
    Serial.println("        Send any character when ready...");
    while (!Serial.available()) delay(100);
    while (Serial.available()) Serial.read();

    scale.set_scale();
    scale.tare(20);
    Serial.println("Tared.");

    Serial.println("\nStep 2: Place known weight on the cell.");
    Serial.print("        Known weight: ");
    Serial.print(CALIBRATION_WEIGHT_G, 1);
    Serial.println(" g");
    Serial.println("        Send any character when stable...");
    while (!Serial.available()) delay(100);
    while (Serial.available()) Serial.read();

    float raw    = scale.get_value(20);
    float factor = raw / CALIBRATION_WEIGHT_G;

    Serial.println("\n========================================");
    Serial.print("  CALIBRATION_FACTOR = ");
    Serial.println(factor, 4);
    Serial.println("  Paste this into the sketch and re-flash.");
    Serial.println("========================================\n");

    // Apply immediately so it works this session without re-flashing
    scale.set_scale(factor);
    scale.tare(10);
}

// ─────────────────────────────────────────────────────────────
//  setup()
// ─────────────────────────────────────────────────────────────
void setup() {
    // USB CDC serial — required for S3-Zero
    // In Arduino IDE: Tools → USB CDC On Boot → Enabled
    Serial.begin(115200);
    delay(2000);
    Serial.println("# Waveshare ESP32-S3-Zero — stiffness logger");

    // RS485 direction pin
    pinMode(RS485_DE_PIN, OUTPUT);
    digitalWrite(RS485_DE_PIN, LOW);

    // RS485 UART
    rs485.begin(MODBUS_BAUD, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN);

    // HX711
    scale.begin(HX711_DT_PIN, HX711_SCK_PIN);
    Serial.print("Waiting for HX711");
    while (!scale.is_ready()) { Serial.print("."); delay(100); }
    Serial.println(" ready.");

    if (CALIBRATION_FACTOR == 0.0f) {
        runCalibration();
    } else {
        scale.set_scale(CALIBRATION_FACTOR);
        Serial.println("Remove all load — taring in 2s...");
        delay(2000);
        scale.tare(20);
        Serial.println("Tared.");
    }

    Serial.println("# timestamp_ms, distance_mm, force_g");
}

// ─────────────────────────────────────────────────────────────
//  loop()
// ─────────────────────────────────────────────────────────────
void loop() {
    static uint32_t lastPoll = 0;
    if (millis() - lastPoll < POLL_INTERVAL_MS) return;
    lastPoll = millis();

    uint32_t ts    = millis();
    float distMM   = readLaserMM();
    float forceG   = scale.is_ready() ? scale.get_units(HX711_READINGS) : NAN;

    Serial.print(ts);
    Serial.print(',');
    if (!isnan(distMM)) Serial.print(distMM, 3); else Serial.print("ERR");
    Serial.print(',');
    if (!isnan(forceG)) Serial.print(forceG, 1); else Serial.print("ERR");
    Serial.println();
}
That's it for now...

Using Visual Studio Code with the ESP-IDE extension and Claude Code

This is a bit of a step for me, having not done any half serious coding for may years. Given the atrophied state of my aged brain, this won...