Project

Light Bar “Relative Comfort” Thermometer with NodeMCU

September 28, 2015 by Patrick Lloyd

Create a wireless "relative comfort" light bar thermometer by broadcasting temperature data across multiple ESP8266 modules running NodeMCU using MQTT.

Technical Concepts

* Cloning code repositories from GitHub
* Connecting to serial port through Linux command line
* Basic Linux Bash
* Basic Circuit Breadboarding

Introduction

Last time we discussed MQTT and its use in low-power embedded applications. That system only used a single remote NodeMCU client and could only blink an RGB LED over the network with commands sent from a Python-based Paho client. This tutorial expands the functionality of our homemade MQTT network into something a little more practical: a thermometer that measures the relative comfort level of the temperature outside. This incorporates two ESP8266 modules running NodeMCU firmware, the MAX31855 thermocouple amplifier which I showed how to make a breakout board for back in August, and an addressable RGB LED strip with WS8612 LEDs. The user chooses a maximum and a minimum temperature that they would consider to be the range of "too hot" to "too cold." An indicator LED will mark where the current temperature is on that scale and give you a quick visual reference on what it feels like outside.

 

Materials Needed

Hardware:

  • 2x ESP8266 Modules
  • 1x Addressable RGB LED Light Strip
    • I used the COM-12027 from Sparkfun but any will do as long as they use WS2812 LEDs
  • MAX31855 Break-Out Board
  • Any K-Type Thermocouple Probe (as long as you can connect it somehow to the MAX31855)
  • 3.3V power supplies for both devices
    • I used a cheap one like this
  • Solderless breadboards

Software:

  • Git
  • Project files
  • Mosquitto
    • ​​mosquitto (the MQTT broker)
    • mosquitto-clients (PUB and SUB clients for MQTT debugging)
  • Esptool (to flash NodeMCU firmware, included in project files repository)
  • Luatool (to upload Lua files to device, included in project files repository)

The project files will be posted along the way, but it is highly recommended that you clone and use the ones from the Github repository. It's much easier to keep updated, and any improvements that either I make or someone else does will be integrated more fluidly.

 

To get the project files, go to a directory of your choosing and type the following into a terminal:

git clone --recursive https://github.com/swedishhat/nodemcu-mqtt-thermo.git
cd nodemcu-mqtt-thermo

 

If an update happens down the line (either to the project files or the submodules), you can update your project folder with:

cd nodemcu-mqtt-thermo
git pull
git submodule sync
git submodule update

Schematics and Hardware

On a system level, the network looks like this:

I'll break it down into further detail when we discuss the individual nodes.

 

A Note on Firmware

The firmware was compiled using Frightanic's NodeMCU Custom Build with the following modules installed: node, file, gpio, wifi, tmr, uart, mqtt, ws2812. Due to a bug in the master branch of the GitHub repository, the dev branch was required. Even though the thermometer module attached to the MAX31855 doesn't need to drive any WS2812 LED's, I used the same firmware with both modules for the sake of simplicity. The firmware file is located in the project repository at:

../nodemcu-mqtt-thermo/firmware/nodemcu-firmware.bin

The firmware can be flashed with Esptool.py (located in the ../nodemcu-mqtt-thermo/tools/esptool directory) with the following command:

../nodemcu-mqtt-thermo/tools/esptool/esptool.py -p (PORT) -b (BAUD) write_flash 0x00000 ../nodemcu-mqtt-thermo/firmware/nodemcu-firmware.bin

If the -p or -b options are removed, the tool defaults to /dev/ttyUSB0 and 9600 respectively.

If you are using an ESP-01 or some ESP8266 breakout that doesn't have a built-in USB-serial adapter like the NodeMCU-Devkit (like the Sparkfun and Adafruit options above) you need to use an external programmer like the FTDI 232RL breakout board. Also, for the ESP-01 in particular, there are some extra hoops to jump through to put the device into firmware flashing mode:

  • Enable the ESP8266 by pulling CH_PD high to VCC. I actually soldered a jumper between those pins on mine so that it is always enabled; I intend to use that ESP8266 as a master microcontroller instead of a slave peripheral
  • Pull GPIO_0 low to GND and cycle power. This actually puts the device in flash mode. Once flashing is complete, remove the connection to ground and cycle power to return to normal mode.

 

The MAX31855 Thermometer Node

This is the ESP8266 that reads the MAX31855 thermocouple amplifier and broadcasts the temperature to the MQTT network. Since the SPI interface for the MAX31855 requires three GPIO pins (SCK, MISO, and ~CS), it needs to connect to the NodeMCU-Devkit or equivalent ESP-12 breakout board. I chose NodeMCU pins D1, D2, and D3 for the SCK, MISO, ~CS pins respectively. This is a picture of my particular setup:

This node is powered off of the USB cable connected to my computer. If you would like to put this outside, you would need to provide the node with an external power source (and probably an enclosure).

 

The WS2812 RGB LED Strip Display Node

The second half of this project is the RGB LED display. This ESP8266 node listens for thermocouple temperature data on the MQTT network and places a blinking marker of the current temperature on a user-customizable scale from "too hot" to "too cold." The WS2812 LED behaves like a shift register, so an arbitrary number of LED's can be connected together in a chain and controlled with a single GPIO pin. My particular WS2812 LED strip from Sparkfun has 60 LEDs, but they are all controlled with my ESP-01's GPIO_2 pin (which is D4 according to NodeMCU for some stupid, confusing reason). The datasheet for the WS2812 says that its normal operating voltage is 5V, but after doing some research and testing it out, the power rail and data line can be reasonably driven with 3.3V (I noticed a bit of flicker but nothing all that distracting). No need to deal with multiple power supplies and level conversion. An image of my ESP-01 setup can be seen below. The ESP-01 is connected to the breadboard through the breadboard adapter and is powered by a 9V battery and a breadboard regulator. My external USB-to-serial converter is at the top in "Plum Crazy Purple".

The Softer Side...

The Base Code

The tool for getting code onto the NodeMCU is luatool.py and is located in the ../nodemcu-mqtt-thermo/tools/luatool/luatool/ directory. The format for uploading files is:

../nodemcu-mqtt-thermo/tools/luatool/luatool/luatool.py -p (PORT) -b (BAUD) -f (SOURCE FILE) -t (DEST FILE) -vr

Like esptool.py, -p and -b are optional and default to /dev/ttyUSB0 and 9600 respectively. "-vr" are the combined verbose and restart-after-upload flags which I find helpful.

I like my code to be as DRY as possible, so I wrote some files for this project to be node-ambiguous. These are the initialization file (i.lua) and the MQTT setup and operating functions (mqtt.lua). They both can be customized with the configuration file specific to each project (config.lua) and there are small tweaks like specifying GPIO pins and MQTT subscribing topics but they are meant to be very generic.

 

The initialization file connects to the WiFi network and creates a table in memory that stores information about the system (IP address, remaining heap size, MAC address, MQTT host, etc.). It then prints that information to the serial port:


-- i.lua for thermometer
-- by Patrick Lloyd

-- Init file, but named something other than init.lua in order to 
-- manually test and debug initialization code.

-- Load all the global user-defined variables
dofile("config.lua")

-- Create system info table and a function to fill it up
sys_info = {}

function get_sys_info()
  -- WiFi Info
  local ip, nm, gw = wifi.sta.getip()
  local mac = wifi.sta.getmac()
  local wifi_mode = {[1] = "STATION", [2] = "SOFTAP", [3] = "STATIONAP"}
  
  -- Hardware Info
  local ver_maj, ver_min, ver_dev, chip_id, flash_id, flash_size, flash_mode,
    flash_speed = node.info()
  local heap_size = node.heap()
  
  sys_info = {
    ["wifi"] = {
      ["WiFi Mode"]   = wifi_mode[wifi.getmode()],
      ["MAC Address"] = mac,
      ["IP Address"]  = ip,
      ["Netmask"]     = nm,
      ["Gateway"]     = gw
    },
    ["sys"] = {
      ["Version"]     = ver_maj.."."..ver_min.."."..ver_dev,
      ["Heap Size"]   = heap_size,
      ["Chip ID"]     = chip_id,
      ["Flash ID"]    = flash_id,
      ["Flash Size"]  = flash_size,
      ["Flash Mode"]  = flash_mode,
      ["Flash Speed"] = flash_speed
    },
    ["mqtt"] = {
      ["Client ID"]   = MQTT_CLIENTID,
      ["MQTT Host"]  = MQTT_HOST..":"..MQTT_PORT
    }
  }
end

-- SW_SPI Pin Initialization
gpio.mode(PIN_CS, gpio.OUTPUT)
gpio.write(PIN_CS, gpio.HIGH)  -- chip not selected
gpio.mode(PIN_SCK, gpio.OUTPUT)
gpio.write(PIN_SCK, gpio.LOW)  -- idle low
gpio.mode(PIN_MISO, gpio.INPUT)

-- Put radio into station mode to connect to network
wifi.setmode(wifi.STATION)

-- Start the connection attempt
wifi.sta.config(WIFI_SSID, WIFI_PASS)

-- Create an alarm to poll the wifi.sta.getip() function once a second
-- If the device hasn't connected yet, blink through the LED colors. If it 
-- has, turn the LED white
tmr.alarm(0, 1000, 1, function()
	if wifi.sta.getip() == nil then print("Connecting to AP...") else
  	-- Refresh the system info table
    get_sys_info()
    
    -- Print all the system info
    print("\n---- System Info ----")
    for keys, vals in pairs(sys_info["sys"]) do print(keys..":\t"..vals) end
    print("")

  	-- Print all the WiFi info
    print("\n---- WiFi Info ----")
    for key, val in pairs(sys_info.wifi) do print(key..":\t"..val) end
    print("")

    tmr.stop(0)         -- Stop the WiFi connect alarm
    dofile("main.lua")  -- Run the main function
   	end
end)

The MQTT file is a little bit more complicated (and took the greatest amount of time to get working in this project!). In my MQTT example from last time, we only published and subscribed to a single topic. I found that there is a fairly significant delay associated with issuing an event like publishing or subscribing and having its callback function complete. Problems arise if the user tries to issue MQTT events too quickly; for example, not all the messages will be published or not all topics will be subscribed to. Some other people were experiencing this issue, so as a solution I wrote a simple semaphore that implements a queue (FILO) with a timer associated with it. The user wants to call an event, and that event is appended to the end of the queue. Every 50 ms, the timer calls the event, and in the callback function for each event, the event removes itself from the queue and shifts the others up. This can be seen below:


--------------------
-- MQTT SEMAPHORE --
--------------------
-- Brevity is the soul of wit. I should write shorter poems...
--print("Hear\nme dear,\ndare I ask you for\na way to write a semaphore\nto queue a table with topics thus\n"..
--"and reduce thy table to digital dust?\nFor time to make a required action\ndelays a timely satisfaction.\n"..
--"But as much as the thought\nof this bothers me ought,\nI cannot avoid it\none little bit.\nNeed a queue?\n"..
--"I do.")

-- These maintain the queues for publish and subscribe topics

-- Builds a queue for topic subscription. Fill it as fast as you want but it
-- will go through each item at a fixed time specified by MQTT_CMDTIME_MS
sub_list = {}
function mqtt_sub_queue(client, topic)
  table.insert(sub_list, {["client"] = client, ["topic"] = topic})
  
  tmr.alarm(1, MQTT_CMDTIME_MS, 1, function()
    if #sub_list > 0 then
      sub_list[1].client:subscribe(sub_list[1].topic, 0, function()
        print("Subscribed to "..sub_list[1].topic)
        table.remove(sub_list, 1)
      end)
    else
      tmr.stop(1)
    end
  end)
end


-- Builds a queue for topic publishing. Fill it as fast as you want but it
-- will go through each item at a fixed time specified by MQTT_CMDTIME_MS
pub_list = {}
function mqtt_pub_queue(client, topic, message)
  table.insert(pub_list, {["client"] = client, ["topic"] = topic, ["message"] = message})
  
  tmr.alarm(2, MQTT_CMDTIME_MS, 1, function()
    if #pub_list > 0 then
      pub_list[1].client:publish(pub_list[1].topic, pub_list[1].message, 0, 0,
        function()
          print("Published \""..pub_list[1].message.."\" to "..pub_list[1].topic)
          table.remove(pub_list, 1)
        end)
    else
      tmr.stop(2)
    end
  end)
end


----------------------------
-- MQTT SETUP AND CONNECT --
----------------------------
MQTT_SUBS = {["/cmd/get_info/"..MQTT_CLIENTID] = mqtt_sys_info}

-- Initialize mqtt client with keepalive timer of 60sec. No password? I too
-- like to live dangerously...
mq = mqtt.Client(MQTT_CLIENTID, 60, "", "")

-- Set up Last Will and Testament (optional)
mq:lwt("/lwt", "Oh noes! Plz! I don't wanna die!", 0, 0)

-- When client connects, print status message and subscribe to cmd topic
mq:on("connect", function(mq) 
  
  -- Serial status message
  print("---- MQTT Info ----")  
  for key, val in pairs(sys_info.mqtt) do print(key..":\t"..val) end

  -- Subscribe to NodeMCU topics using semaphore stuff above
  for i,_ in pairs(MQTT_SUBS) do mqtt_sub_queue(mq, i) end  
  
  print("")
  main_loop()

end)


-- When client disconnects, print a message and list space left on stack
mq:on("offline", function()
  print ("\nDisconnected from broker")
  print("Heap:\t"..node.heap().."\n")
end)


-- On a publish message receive event, run the message dispatcher and interpret the command
mq:on("message", function(mq,t,pl)
  -- It allows different functions to be run based on the message topic
  if pl ~= nil and MQTT_SUBS[t] ~= nil then MQTT_SUBS[t](mq, pl) end
end)

-- Connect to the broker
mq:connect(MQTT_HOST, MQTT_PORT, 0, 1)

The rest of the code is fairly node-specific so we'll break it down down separately. The only remaining commonality lives in the configuration file since both nodes need to specify the same WiFi SSID and password and MQTT host information. Each MQTT client ID is separate, however. 

 

The MAX31855 Thermometer Node

In the rest of the configuration file, we just need to specify names for the pin numbers that the MAX31855 will connect to. Everything else is taken care of in the main function.


-- config.lua for thermometer
-- by Patrick Lloyd

-- Global variable configuration file for better portability
-- Change for your particular setup. This assumes default Mosquitto config

-- Pin Declarations
PIN_SCK = 1
PIN_MISO = 2
PIN_CS = 3

-- WiFi
WIFI_SSID = ""
WIFI_PASS = ""

-- MQTT
MQTT_CLIENTID   = "esp-therm"
MQTT_HOST       = ""
MQTT_PORT       = 1883
MQTT_CMDTIME_MS = 500

print("\nConfig complete")

The main.lua file is where all the action takes place, if you can imagine. I first tried to build a Lua module to control the MAX31855 with public and private functions but it was too big for the ESP8266 to keep in memory. I moved the basic functionality to the main.lua file and all "private" functions (not meant to be run directly) were prepended with an underscore. The first of those is _read32(). This sets and reads the GPIO pins on the SPI bus. There is technically a hardware SPI module in the ESP8266 but I was having trouble getting it working properly with NodeMCU (the documentation is somewhat poor on this feature), so in lieu of that, I just manually controlled the bus lines, which is also known as bit-banging.

The next private function is _temp_decode(), which takes a string of the temperature bits read off the SPI bus and converts it to a human-readable temperature in degrees Celsius. After that, the public functions begin. mqtt_update_temp() reads the temperature, converts it, and sends it out on the MQTT network to be read by other devices. mqtt_sys_info is a helper function meant to publish system information on the MQTT network for debugging. Finally, the main_loop() just starts a timer to call mqtt_update_temp() every five seconds. main_loop() is called by mq:on("connect", function(mq)​ in the mqtt.lua file.


-- main.lua for thermometer
-- by Patrick Lloyd

--------------------------------
-- HARDWARE CONTROL FUNCTIONS --
--------------------------------

-- Bit-bang SPI bus to update 'raw' table
reading_bus = false

function _read32()
  local raw = {}

  -- Setting this flag allows functions to wait for data in a blocking loop
  reading_bus = true
  
  -- Select chip and give it a microsecond to become active
  gpio.write(PIN_CS, gpio.LOW)
  tmr.delay(1)

  -- Cycle clock and read bus data into 'raw' 32 times
  for i = 1, 32 do
    gpio.write(PIN_SCK, gpio.HIGH)
    raw[i] = gpio.read(PIN_MISO)
    gpio.write(PIN_SCK, gpio.LOW)
    tmr.delay(1)
  end
  
  -- Deselect chip, wait 1 us, clear "busy" flag
  gpio.write(PIN_CS, gpio.HIGH)
  tmr.delay(1)
  reading_bus = false

  return raw
end


-- Decodes temperature values either for TC or reference junction depending on the bit width
function _temp_decode(bin_value)
  
  -- Ignore sign bit for now and convert to decimal number
  local temp_c = tonumber(string.sub(bin_value, 2), 2)
  
  -- Heed the sign bit! 
  if string.sub(bin_value, 1, 1) == 1 then
    temp_c = temp_c * -1
  end

  -- Differentiate between TC or RJ and scale appropriately
  if #bin_value == 14 then
    temp_c = temp_c * 0.25
  elseif #bin_value == 12 then
    temp_c = temp_c * 0.0625
  end

  return temp_c
end


-- Return a table with floating point temperature values and the error bits
-- Sometimes you will get ridiculous (yet legal) temperature values when
-- certain errors happen. This puts error checking responsibility on the
-- receiving system, if it cares about such things.
function mqtt_update_temp(mq)
  -- Update 'raw' data and wait for it to finish
  local data = _read32()
  while reading_bus do end

  -- Make sure the argument is legal
  --err_msg = "\nERROR: Device argument for max31855_swspi.temp() not recognized.\nOptions are \"tc\" for thermocouple or \"rj\" for Reference Junction.")
  mqtt_pub_queue(mq, "/data/temp/rj", _temp_decode(table.concat(data, "", 17, 28)))
  mqtt_pub_queue(mq, "/data/temp/tc", _temp_decode(table.concat(data, "", 1, 14)))
  mqtt_pub_queue(mq, "/data/temp/err", table.concat(data, "", 30, 32))
end




-- Print and publish system info like at bootup but do it whenever
function mqtt_sys_info(mq, pl)
  get_sys_info()
  local err_msg = "\nERROR: MQTT payload for mqtt_sys_info() not a valid argument\nOptions are \"wifi\", \"sys\", or \"mqtt\"."

  if sys_info[pl] == nil then print(err_msg) else 
    for key, val in pairs(sys_info[pl]) do mqtt_pub_queue(mq, "/status/"..MQTT_CLIENTID.."/"..pl, key..":\t"..val) end
  end
end


function main_loop()
  tmr.alarm(5, 5000, 1, function() mqtt_update_temp(mq) end)
end

-- Load up the MQTT functions and variables
dofile("mqtt.lua")


The WS2812 RGB LED Strip Display Node

In this config file, in addition to WiFi and MQTT constants, we need to setup the maximum and minimum "comfortable temperatures" and create a string which holds the RGB values for each of the LED's on the scale. This color table takes the form TEMP_COLOR_LUT = string.char(R1, G1, B1, R2, G2, B2, ... , R60, G60, B60) where R# is a number between 0 and 255. Since I have 60 LEDs in my strip, my table has 180 elements. NodeMCU provides a WS2812.writergb() function to control the strip that accepts only the pin number for the data line and an ASCII string (since ASCII characters can be represented with eight bits [0-255]). TEMP_COLOR_LUT in this case goes from blue to green to red on what's known as inverse HSV gradient.


-- config.lua for light strip
-- by Patrick Lloyd

-- Global variable configuration file for better portability
-- Change for your particular setup. This assumes default Mosquitto config


-- Pin Declarations
PIN_WS2812 = 4  -- This is GPIO2 on ESP8266. No idea why NodeMCU does this...

-- WiFi
WIFI_SSID = ""
WIFI_PASS = ""

-- MQTT
MQTT_CLIENTID   = "esp-led"
MQTT_HOST       = ""
MQTT_PORT       = 1883
MQTT_CMDTIME_MS = 50

-- Upper and lower temperature bounds for comfort (deg C)
TEMP_MAX = 44.0     -- Too hot!
TEMP_MIN = -7.0     -- Brrr!

-- HSV Temperature Color Table in form of Inverse HSV Gradient based off of this tool: http://www.perbang.dk/rgbgradient/
TEMP_COLOR_LUT = string.char(
    0, 0, 255, 0, 17, 255, 0, 34, 255, 0, 51, 255, 0, 69, 255, 0, 86, 255, 0, 103, 255, 0, 121, 255, 0, 138, 255, 0, 155, 255, 0, 172, 255, 0, 190, 255,
    0, 207, 255, 0, 224, 255, 0, 242, 255, 0, 255, 250, 0, 255, 233, 0, 255, 216, 0, 255, 198, 0, 255, 181, 0, 255, 164, 0, 255, 146, 0, 255, 129, 0,
    255, 112, 0, 255, 95, 0, 255, 77, 0, 255, 60, 0, 255, 43, 0, 255, 25, 0, 255, 8, 8, 255, 0, 25, 255, 0, 43, 255, 0, 60, 255, 0, 77, 255, 0, 95,
    255, 0, 112, 255, 0, 129, 255, 0, 146, 255, 0, 164, 255, 0, 181, 255, 0, 198, 255, 0, 216, 255, 0, 233, 255, 0, 250, 255, 0, 255, 242, 0, 255, 224,
    0, 255, 207, 0, 255, 190, 0, 255, 172, 0, 255, 155, 0, 255, 138, 0, 255, 121, 0, 255, 103, 0, 255, 86, 0, 255, 69, 0, 255, 51, 0, 255, 34, 0, 255,
    17, 0, 255, 0, 0)

print("\nConfig complete")

In the main.lua file for the LED strip node, we begin by writing the TEMP_COLOR_LUT string to the light strip just so we have something pretty to look at while we wait for temp data to arrive through the network. Then, on to function declarations. temp_position() determines the position of the indication LED on the strip based off of the measured  temperature, the number of elements in TEMP_COLOR_LUT, and the bounds set by TEMP_MAX and TEMP_MIN. update_led_strip() is what blinks the indicator LED and actually calls the WS2812.writergb() function to change the LED color. mqtt_temp_update() is a callback function that is run whenever new temperature data shows up on the MQTT network and mqtt_sys_info() is like the one from the previous node that just provides system info for debugging. main_loop() doesn't actually do anything but is left in so that minimal changes have to be made to the mqtt.lua file that was reused between nodes.


-- main.lua for led strip
-- by Patrick Lloyd

--------------------------------
-- HARDWARE CONTROL FUNCTIONS --
--------------------------------
-- OoOoOoOo shiny!
ws2812.writergb(PIN_WS2812, TEMP_COLOR_LUT)

-- Determine position of relative temperature indicator
function temp_position(temp)
  -- Check if temp is in correct range. Stupid Lua trick adapted from http://lua-users.org/wiki/TernaryOperator
  -- The +0.001 is so that 'pos' never evaluates to zero during normalization
  local t = (temp > TEMP_MAX and TEMP_MAX) or (temp <= TEMP_MIN and TEMP_MIN + 0.001) or temp

  -- Normalize temp in range and scale to LED strip. It's just algebra, bruh.
  local pos = ((t - TEMP_MIN) * #TEMP_COLOR_LUT / 3.0) / (TEMP_MAX - TEMP_MIN)

  -- Round up to nearest integer
  return math.ceil(pos)
end

-- Write to the LED strip
function update_led_strip(temp, on_off)
  local err_msg = "\nERROR: On/Off argument for update_led_strip() not recognized.\nOptions are \"on\" or \"off\"."

  local str_pos_end = 3 * temp_position(temp)
  local ind_led = {["on"] = string.char(255,255,255), ["off"] = string.char(0, 0, 0)}
  --local displaced = TEMP_COLOR_LUT:sub(str_pos_end-2, str_pos_end)

  if ind_led[on_off] == nil then print(err_ms) else
    -- It doesn't toss errors if substrings are out of bounds! Holy cow!
    ws2812.writergb(PIN_WS2812, TEMP_COLOR_LUT:sub(1, str_pos_end - 3)..ind_led[on_off]..TEMP_COLOR_LUT:sub(str_pos_end + 1))
  end
end

-- Receive MQTT temp data and parse it, then call update_led_strip().
INDICATOR_ON = true
function mqtt_temp_update(mq, pl)
  local on_off = {[true] = "on", [false] = "off"}
  update_led_strip(tonumber(pl), on_off[INDICATOR_ON])
  INDICATOR_ON = not INDICATOR_ON
end

-- Print and publish system info like at bootup but do it whenever
function mqtt_sys_info(mq, pl)
  get_sys_info()
  local err_msg = "\nERROR: MQTT payload for mqtt_sys_info() not a valid argument\nOptions are \"wifi\", \"sys\", or \"mqtt\"."

  if sys_info[pl] == nil then print(err_msg) else 
    for key, val in pairs(sys_info[pl]) do mqtt_pub_queue(mq, "/status/"..MQTT_CLIENTID.."/"..pl, key..":\t"..val) end
  end
end


function main_loop()
end

-- Load up the MQTT functions and variables
dofile("mqtt.lua")


Small Bonus Script

But wait! There's more! Clone the repository in the next ten minutes and receive a helper Bash script that uploads all the files to the NodeMCU and runs GNU Screen. This is not designed to work with all systems and setups so your mileage may vary.

 

Putting Everything Together

Assuming everything is uploaded and playing nice with each other, the system should now respond to changes in temperature and display them accordingly. Here is a demo video I made testing two different temperatures of water against ambient.

The next video demonstrates running the mosquitto broker and publishing / subscribing to topics with the mosquitto_pub and mosquitto_sub clients. It demonstrates reading temp data and calling the command to publish system information. This was done with the Paho client in the last article.

Conclusion

From this project, it can be seen that the NodeMCU platform can be scaled to make some fairly complex projects, and support for more devices is constantly being added. In future installments, I would like to explore how to make a DIY reflow oven controller and incorporate feedback control systems into the programs. I'd also like to investigate writing applications with native C code which could speed up certain parts of the process and give the user a good taste of the ESP8266 C API using esp-open-sdk. Until next time, happy hacking.

 

Give this project a try for yourself! Get the BOM.

5 Comments
  • Tchavdar Naydenov November 23, 2015

    Excellent tutorial Patrick. Thank you for taking the time to share it.
    A K-Type Thermocouple Probe and a MAX31855 seems to be overkill for measuring the comfort level. You can do this with a DHT22 much easily but I needed information how to use the MAX31855 with NodeMCU and you have provided all the information that I needed.

    Like. Reply
  • Martin Ziegler December 08, 2015

    Hi Patrick, nice Tutorial…

    Have a tried to build all this not with a NodeMCU but with an ESP-12 only.
    Script works fine, the connection to the MQTT Broker works also good.
    But my MAX sends wrong data. I’ve connected the MAX to the following Pins:
    PIN_SCK = 5
    PIN_MISO = 4
    PIN_CS = 0

    the Result is:
    Published “127.9375” to /data/temp/rj
    Published “2047.75” to /data/temp/tc
    Published “111” to /data/temp/err

    do you (or anybody else) have an Idea ?

    Cheers Martin

    Like. Reply
  • P
    Patrick Lloyd December 08, 2015

    @Tchavdar: Thanks! This is one of those examples of using what’s already in the parts bin vs. going out and buying a new sensor. Originally got the thermocouple for measuring oven temps (~400C).

    @Martin: The error bits that are being transmitted make me think that the proper connections are not being made. If you check the datasheet, all three bits mean there’s a short circuit to ground, a short circuit to vcc and an open circuit which doesn’t make a lot of physical sense. Take special care to ensure that the pins you connected the MAX to are also set up correctly in the config.lua file. The pins may be moved around a bit between the NodeMCU and the ESP-12.

    Like. Reply