Alright, it’s time to make our smelter actually do something. After a long night I have the majority of our code, if not all, ready to go and probably buggy with the help of ChatGPT.

Lets start with our NodeMCU / Ardunio / ESP8266. We’ll need the arduino IDE set up for this with the ESP libraries installed and all that. I already have that built and that set up is beyond the scope of this article. It’s pretty easy these days, just go to the library manager and add it.

Get the parts to build this here.

Anyway, here is the code for the microcontroller (NodeMCU, Arduino, ESP8266, Microcontroller, same difference):

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Adafruit_MAX31855.h>

// Replace with your WiFi credentials
const char *ssid = "your_SSID";
const char *password = "your_PASSWORD";

const int outputPin = D1; // Power output pin
const int safetyPin = D2; // Thermal runaway safety pin
const int thermoCLK = D3;
const int thermoCS = D4;
const int thermoDO = D5;
const unsigned int localPort = 3000; // Local port to listen on

Adafruit_MAX31855 thermocouple(thermoCLK, thermoCS, thermoDO);

WiFiUDP udp;

// State machine
enum State { IDLE, PROCESS_UDP, CONTROL_TEMPERATURE, WATCHDOG_CHECK };
State currentState = IDLE;

unsigned long timerStart;
unsigned long timerValue;
float targetTemperature;
unsigned long lastWatchdogReceived;
const unsigned long watchdogTimeout = 15000; // Adjust the timeout as needed

void setup() {
  Serial.begin(115200);
  pinMode(outputPin, OUTPUT);
  pinMode(safetyPin, OUTPUT);
  digitalWrite(safetyPin, HIGH);

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected to Wi-Fi");

  // Initialize UDP
  udp.begin(localPort);
  Serial.printf("Listening on UDP port %d\n", localPort);

  // Initialize thermocouple
  thermocouple.begin();
}

void loop() {
  switch (currentState) {
    case IDLE:
      checkForUdpPacket();
      break;
    case PROCESS_UDP:
      processUdpPacket();
      break;
    case CONTROL_TEMPERATURE:
      controlTemperature();
      break;
    case WATCHDOG_CHECK:
      processWatchdog();
      break;
  }
}

void checkForUdpPacket() {
  int packetSize = udp.parsePacket();
  if (packetSize) {
    char packetType;
    udp.read(&packetType, 1);
    if (packetType == 'W') {
      currentState = WATCHDOG_CHECK;
    } else {
      currentState = PROCESS_UDP;
    }
  }
}

void processUdpPacket() {
  int packetSize = udp.available();
  char incomingData[packetSize];
  udp.read(incomingData, packetSize);

  // Parse timer and target temperature from the received data
  char *token = strtok(incomingData, ",");
  timerValue = atol(token);
  token = strtok(NULL, ",");
  targetTemperature = atof(token);

  timerStart = millis();
  currentState = CONTROL_TEMPERATURE;
}

void controlTemperature() {
  if (millis() - timerStart < timerValue) {
    float currentTemperature = thermocouple.readCelsius();
    if (isnan(currentTemperature)) {
      Serial.println("Error reading temperature");
    } else {
      Serial.print("Current temperature: ");
      Serial.println(currentTemperature);

      if (currentTemperature < targetTemperature) {
        digitalWrite(outputPin, HIGH);
      } else {
        digitalWrite(outputPin, LOW);
      }

      // Detect thermal runaway
      if (currentTemperature > targetTemperature + 10) { // Adjust the threshold as needed
        digitalWrite(outputPin, LOW);
        digitalWrite(safetyPin, LOW);
        currentState = IDLE;
      }
    }
  } else {
    digitalWrite(outputPin, LOW);

    if (millis() - lastWatchdogReceived > watchdogTimeout) {
      digitalWrite(safetyPin, LOW);
    } else {
      digitalWrite(safetyPin, HIGH);
    }

    currentState = IDLE;
  }
}

void processWatchdog() {
  lastWatchdogReceived = millis();
  IPAddress remoteIp = udp.remoteIP();
  uint16_t remotePort = udp.remotePort();
  float currentTemperature = thermocouple.readCelsius();

  char temperatureStr[10];
  dtostrf(currentTemperature, 6, 2, temperatureStr);
  udp.beginPacket(remoteIp, remotePort);
  udp.write(temperatureStr);
  udp.endPacket();

  currentState = IDLE;
}

So that’s C++ there. I’ll let you in on a little secret, I didn’t exactly write that. That code was generated by ChatGPT4 and I’ll tell you what. That thing isn’t taking a programmer’s job just yet. I had to make it make numerous revisions and corrections to the code which required me to know how to explain what I wanted the code to do in algorithm speak and also required me to be able to read the code to ensure it did what it was supposed to do.

See the article “Coding with ChatGPT” to get a full breakdown on this process.

So we’ll deposit that code on the NodeMCU.

Now we need a server. When I say server I mean a lot of things. We need a host, I’m partial to supermicro; we need a hypervisor, I like vmware, kinda; we need a VM, Debian is my favorite; and lastly we need the server application. We’re only going to discuss the Node.JS server application here. The rest of that perhaps we’ll discuss someday, there’s at least a million tutorials out there for each.

So our node.js app needs to do a few things.

  1. Web form input to receive the series of timer values and temperatures
  2. Server the web content
  3. Pass each round of timer and temp to the NodeMCU
  4. Time the timer and pass the next stage of values once the time expires
  5. Be a watchdog to notify us if the NodeMCU crashes (unfortunately that is not uncommon)
  6. Receive status updates from the NodeMCU (time and temp)
  7. Pass the current time and temp from the NodeMCU to a web page for monitoring

And here’s the code:

const express = require('express');
const bodyParser = require('body-parser');
const dgram = require('dgram');
const axios = require('axios');

const udpClient = dgram.createSocket('udp4');
const app = express();
const port = 3000;

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

let timerTemperatureArray = [];

app.get('/', (req, res) => {
  res.send(generateHtml(timerTemperatureArray));
});

app.post('/submit', (req, res) => {
  const { timer, temperature } = req.body;

  if (Array.isArray(timer) && Array.isArray(temperature) && timer.length === temperature.length) {
    timerTemperatureArray = timer.map((t, i) => ({
      timer: parseFloat(t),
      temperature: parseFloat(temperature[i]),
    }));

    processTimers();
  }

  res.redirect('/');
});

app.listen(port, () => {
  console.log(`Node.js app listening on port ${port}`);
});

function generateHtml(timerTemperatureArray) {
  const arrayHtml = timerTemperatureArray
    .map((entry) => `<li>Timer: ${entry.timer} min, Temperature: ${entry.temperature} °F</li>`)
    .join('');

  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Temperature Control Form</title>
      <script>
        function addRow() {
          const table = document.getElementById('tempTable');
          const row = table.insertRow(-1);
          const cell1 = row.insertCell(0);
          const cell2 = row.insertCell(1);
          cell1.innerHTML = '<input type="number" name="timer[]" min="0" step="0.1" required>';
          cell2.innerHTML = '<input type="number" name="temperature[]" min="0" step="0.1" required>';
        }
      </script>
    </head>
    <body>
      <h1>Temperature Control Form</h1>
      <form method="POST" action="/submit">
        <table id="tempTable">
          <tr>
            <th>Timer (minutes)</th>
            <th>Temperature (°F)</th>
          </tr>
          <tr>
            <td><input type="number" name="timer[]" min="0" step="0.1" required></td>
            <td><input type="number" name="temperature[]" min="0" step="0.1" required></td>
          </tr>
        </table>
        <button type="button" onclick="addRow()">Add Row</button>
        <br><br>
        <input type="submit" value="Submit">
      </form>
      <h2>Current Timer and Temperature Array</h2>
      <ul>${arrayHtml}</ul>
    </body>
    </html>
  `;
}

async function processTimers() {
  for (const entry of timerTemperatureArray) {
    const timerMs = entry.timer * 60 * 1000;
    const temperatureC = (entry.temperature - 32) * (
5 / 9);
const message = Buffer.from(JSON.stringify({ timer: timerMs, temperature: temperatureC }));
udpClient.send(message, 0, message.length, 41234, 'localhost', (err) => {
  if (err) {
    console.log('Error sending UDP packet:', err);
    return;
  }
  console.log(`Sent packet: timer = ${timerMs} ms, temperature = ${temperatureC} °C`);
});

await new Promise((resolve) => setTimeout(resolve, timerMs));

// Watchdog signal
const watchdogMessage = Buffer.from('watchdog');
udpClient.send(watchdogMessage, 0, watchdogMessage.length, 41235, 'localhost', (err) => {
  if (err) {
    console.log('Error sending watchdog signal:', err);
    return;
  }
  console.log('Sent watchdog signal');
});

// Receive watchdog response
udpClient.once('message', async (msg, rinfo) => {
  const watchdogResponse = JSON.parse(msg.toString());
  const remainingTimeMs = watchdogResponse.timer;
  const remainingTimeMin = (remainingTimeMs / 1000 / 60).toFixed(1);
  const currentTemperatureF = (watchdogResponse.temperature * (9 / 5)) + 32;

  // Send remaining timer and current temperature to the monitor app
  try {
    await axios.post('http://localhost:4000/update', {
      remainingTime: remainingTimeMin,
      currentTemperature: currentTemperatureF.toFixed(1),
    });
    console.log('Sent remaining timer and current temperature to the monitor app');
  } catch (err) {
    console.error('Error sending data to the monitor app:', err);
  }
});
}
}

The code above may not be right. ChatGPT kind of lost it in there. The code blocks for formatting the text display were out of place so parts of the code were outside the code block and it just stopped outputting about half way through and I had to prompt it to continue. I’ll update it once I launch it on our server.

Next we need some code to display the current situation in a web page.

const express = require('express');
const bodyParser = require('body-parser');

const app = express();
const port = 4000;
const temperatureThreshold = 1.0; // Change threshold for temperature
const timerThreshold = 1000; // Change threshold for timer

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

let currentTemperature = null;
let remainingTimer = null;

app.post('/temperatureUpdate', (req, res) => {
  const { temperature, remainingTimer: receivedTimer } = req.body;
  const temperatureChanged = Math.abs(currentTemperature - temperature) >= temperatureThreshold;
  const timerChanged = Math.abs(remainingTimer - receivedTimer) >= timerThreshold;

  if (temperatureChanged || timerChanged) {
    currentTemperature = temperature;
    remainingTimer = receivedTimer;
  }

  res.sendStatus(200);
});

app.get('/', (req, res) => {
  res.send(generateHtml(currentTemperature, remainingTimer));
});

app.listen(port, () => {
  console.log(`Temperature display app listening on port ${port}`);
});

function generateHtml(temperature, timer) {
  const temperatureF = temperature ? (temperature * 9 / 5) + 32 : null;
  const timerMin = timer ? timer / 60000 : null;

  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Temperature and Timer</title>
      <meta http-equiv="refresh" content="60">
    </head>
    <body>
      <h1>Temperature and Timer Status</h1>
      <p>Current temperature: ${temperatureF ? `${temperatureF.toFixed(2)} °F` : 'N/A'}</p>
      <p>Remaining timer: ${timerMin ? `${timerMin.toFixed(1)} minutes` : 'N/A'}</p>
      <p>
        <a href="http://localhost:3000">Timer and Temperature Entry Page</a>
      </p>
    </body>
    </html>
  `;
}

And that’s our code for now. Think it’s at all functional? Lets find out.

Well it has a lot of problems. For one it will only run as root but that may be on me. Also the UDP listener appears to be suffering from hearing loss. The html pages are really decent. Pretty handy not to have to mess around with a web server or any of that.

I’ve spent about 5 hours refining the code and asking chatGPT to rewrite sections and it’s still not usable. I’ll come back with some good code later and we’ll wrap this up.

Until then…

4 Replies to “DIY Computerized Electric Smelter/Furnace/Oven/Kiln – Part 2”

Leave a Reply

Your email address will not be published. Required fields are marked *