M0AGX / LB9MG

Amateur radio and embedded systems

NodeMCU networked thermometer

In the last post I made a short introduction on development using the NodeMCU platform with a simple reading of a 1-wire thermometer. This time I will show how to make a basic networked application for the NodeMCU and the server part for a computer to gather some useful data.

The idea is pretty simple: do a temperature conversion, connect to a wireless network, transmit the reading to a server, maybe get back some configuration and start over.

The code

Complete code is at the end of the post.

Configuration

1
2
3
4
5
sensor_id=2; --logical sensor number
pin=2; --onewire pin number
tcp_port=2500; --server port
tcp_addr="192.168.10.182"; --server IP, CHANGE TO SUIT YOUR NEEDS
global_sleep=5000000; --default sleep time in microseconds (5s)

Conversion and network connection

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--temperature conversion and WiFi connection
ow.setup(pin);
p=ow.reset(pin);
print("present="..p);
ow.skip(pin);
ow.write(pin, 0x44, 1);--CONVERT T command

wifi.setmode(wifi.STATION);
wifi.sta.config("YOUR_WIRELESS_NETWORK_NAME","YOUR_NETWORK_PASSWORD");
wifi.sta.connect();

First part is identical as in the first post - it sends a temperature conversion request to a DS18B20, the second part connects to the wireless network (YES - it is that easy). Of course the name and password has to be changed.

Checking for network connection

1
2
3
4
5
6
--Check for an ip every second and do something.
tmr.alarm(0,1000, 1, function() 
   if wifi.sta.getip()==nil then
       print("connecting to AP...") 
   else
       print('ip: ',wifi.sta.getip())

Establishing a connection to a wireless network and getting an IP via DHCP takes some time (in the order of many seconds), so we have to wait until the connection is ready in order to transmit data. This code makes a timer that executes every 1s. A function is declared inside the invocation of tmr.alarm (the last argument is the function to be executed at the desired interval) - Lua allows that. That is the "main" function of my application. Of course a function can be declared separately - then the tmr.alarm argument would be the function's name.

When an IP is finally assigned and network is ready then the core part can start.

Temperature reading and conversion to centigrade

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
p2=ow.reset(pin);
print("present2="..p2);
ow.skip(pin);
ow.write(pin,0xBE,1); --READ SCRATCHPAD command
lsb=ow.read(pin);
msb=ow.read(pin);

temp=bit.lshift(msb,8)+lsb;
print("msb="..msb.." lsb="..lsb.." temp="..temp);

if (temp > 32767) then --convert from two's complement to decimal
    temp = temp - 65536;
end

Tc100=(6*temp)+temp/4; --magic formula to multiply by 0.0625*100 (1bit=0.0625 degrees C, the result is in 0,01degC)

print("temp="..temp);

temperature=string.format("%d",Tc100);
battery=adc.read(0);
msg=sensor_id.."!"..temperature.."!"..battery;
print("msg="..msg);

It starts with a read from the DS18B20, conversion from two's complement (it's really done like this in Lua...) to decimal and then converted to centigrade. The sensor outputs temperature in 0.0625 degrees per bit by default (so you have to multiply raw result from the sensor by this factor). To make it a little bit easier the result is scaled by 100 to avoid much floating-point math (it's computationally expensive on small microcontrollers = battery hungry).

The last piece formats the temperature as an integer, reads the ADC (I want to make a battery powered, standalone sensor eventually) and builds a message string that contains the logical sensor number, temperature and battery voltage. All separated by exclamation marks (any other character also will do).

Here comes the networking part:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
c=net.createConnection(net.TCP, 0);
c:on("receive", function(sck, rec) 
        sck:send(msg);
        print(rec);
        global_sleep=rec;
    end );
c:on("sent", function(sck, c)
        sck:close();
        print("sent")
    end );
c:on("disconnection", function(sck, c)
        sck:close();
        print("disconnect");
        print(global_sleep);
        if (tonumber(global_sleep)>0) then
            node.dsleep(global_sleep);
        end;
    end );
c:connect(tcp_port, tcp_addr); --everything else is event driven from this moment
tmr.stop(0)
    end
end)

Connection handling on the NodeMCU is event-driven. In layman's terms: it means that you have to set up your own functions that will be executed by the "operating system" when an event occurs (you don't know when it will happen), eg. connection establishment, new data being received, disconnection etc.

In my scenario I first create a TCP client socket (called here c) and then register my events. The first argument of the callback functions is the socket that applies to them, eg. you can register one callback function for many sockets. It can be useful when NodeMCU has the role of a TCP server and multiple clients ask for the same data. It makes sense to write just a single function to handle all requests.

"Receive" event is executed when data is being received from the server. Currently the server just sends the desired amount of time NodeMCU should sleep before making another measurement. That event sends the msg string to the server.

"Sent" event is executed when all outgoing data has been sent (ie. the sck:send(msg) from "receive" event completes) - NodeMCU simply disconnects.

When disconnection is complete the program checks if the data received from the server is a positive number and then puts the device into deep sleep. Deep sleep dramatically reduces power consumption to several microamperes. The MCU halts and after the set time is reset (an extra wire between D0 and RST pin is required for it to work), there is no way to preserve the application state or data in RAM.

Autostart

The main code can be uploaded to the NodeMCU with luatool, however it should start automatically after reset (to use the deep sleep feature). The firmware looks for a file called init.lua at startup and tries to execute it. In my case is simply contains a single line:

1
dofile('gettemperature.lua');

Beware: if your scripts has a bug, adding it to init.lua will lock you out of controlling the device with a serial terminal and then you will have to restore the device to a clean state. I use a negative "interval" sent by the server to instruct the device not to sleep and to regain manual control over it.

Server

The server is a very simple PHP script - just a plain TCP socket server that listens on port 2500 for clients. When a client connects (the function socket_accept will block until there it is a connection) it sends the vaule of $reply (it fires the "receive" event on NodeMCU), reads data the client has sent, prints it out alongside date and time and disconnects (then the "disconnect" event is fired on NodeMCU and it goes to sleep).

The data packet can be split using the exclamation mark as a separator and individual fields are stored in $sensor_id, $temperature, $battery (the raw value from the ADC, not voltage).

The server can handle only one cilent at a time, but thanks to TCP, if there are many clients connecting at once, they will automatically wait, as the server loop executes quickly enough not to make those connection requests time out.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/php
<?PHP
date_default_timezone_set('Europe/London');

$reply='10000000'; //10s
$reply='-10';

$socket = socket_create(AF_INET, SOCK_STREAM, 0);
if ( socket_bind($socket, "0.0.0.0" , 2500) === FALSE ) { exit(1); }
socket_listen( $socket, 0 );

while(1) {
    $connection = socket_accept($socket); 
    socket_send($connection, $reply, strlen($reply), 0); //tell the sensor how long should it sleep
    socket_recv($connection, $pkt, 1024, 0);
    if (strlen($pkt)>0){
        echo date("H:i:s")." Data: ".trim($pkt)."\n";
        $field=explode('!',$pkt);
        $sensor_id='no'.$field[0];
        $temperature=$field[1]/100;
        $voltage=$field[2];
    }
    socket_close($connection);
}

Example output from the server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ ./nodemcu_relay_demo.php 
22:04:06 Data: 2!2537!1024
22:04:19 Data: 2!2537!1024
22:04:33 Data: 2!2537!1024
22:04:49 Data: 2!2537!1024
22:05:03 Data: 2!2531!1024
22:05:17 Data: 2!2537!1024
22:05:30 Data: 2!2537!1024
22:05:43 Data: 2!2531!1024
22:05:57 Data: 2!2537!1024
22:06:10 Data: 2!2531!1024
22:06:24 Data: 2!2537!1024
22:06:37 Data: 2!2537!1024
22:06:51 Data: 2!2537!1024

The data does not come exactly every 10 seconds, but close enough.

What's next?

I want to build an autonomous sensor to be battery powered and installed outdoors. I want to achieve at least half year battery life, as WiFi is not the most power efficient wireless technology, perhaps using the built-in filesystem to take measurements over a longer piece of time and send them all at once.

At first I tried to transmit data using UDP as it perfectly fits such small amounts of data with less overhead than TCP (but it offers no reliability), however it turned out to not work at all right after startup, even if an IP has been assigned by DHCP. Waiting some seconds (and consuming power) and blindly sending data would not be a smart choice. I also had to send data in the "receive" event, as doing it right after "connect" event also sent it nowhere (there was no TCP traffic visible in Wireshark on the server). If you first send data from the remote end to NodeMCU everything looks fine, at least if you do it right after power-up. Maybe it works reliably after some time when the device is not repeatably restarted.

I will also make a post how to use RRDtool to store data and make such graphs: Indoor temperature over 7 days

Complete source code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
sensor_id=2; --logical sensor number
pin=2; --onewire pin number
tcp_port=2500; --server port
tcp_addr="192.168.10.182"; --server IP, CHANGE TO SUIT YOUR NEEDS
global_sleep=5000000; --default sleep time in microseconds (5s)


ow.setup(pin);
p=ow.reset(pin);
print("present="..p);
ow.skip(pin);
ow.write(pin, 0x44, 1);--CONVERT T command

wifi.setmode(wifi.STATION);
wifi.sta.config("YOUR_WIRELESS_NETWORK_NAME","YOUR_NETWORK_PASSWORD");
wifi.sta.connect();

--Check for an ip every second and do something.
tmr.alarm(0,1000, 1, function() 
    if wifi.sta.getip()==nil then
        print("connecting to AP...") 
    else
        print('ip: ',wifi.sta.getip())
        p2=ow.reset(pin);
        print("present2="..p2);
        ow.skip(pin);
        ow.write(pin,0xBE,1); --READ SCRATCHPAD command
        lsb=ow.read(pin);
        msb=ow.read(pin);

        temp=bit.lshift(msb,8)+lsb;
        print("msb="..msb.." lsb="..lsb.." temp="..temp);

        if (temp > 32767) then --convert from two's complement to decimal
            temp = temp - 65536;
        end

        Tc100=(6*temp)+temp/4; --magic formula to multiply by 0.0625*100 (1bit=0.0625 degrees C, the result is in 0,01degC)

        print("temp="..temp);

        temperature=string.format("%d",Tc100);
        battery=adc.read(0);
        msg=sensor_id.."!"..temperature.."!"..battery;
        print("msg="..msg);
        c=net.createConnection(net.TCP, 0);
        c:on("receive", function(sck, rec) 
                sck:send(msg);
                print(rec);
                global_sleep=rec;
            end );
        c:on("sent", function(sck, c)
                sck:close();
                print("sent")
            end );
        c:on("disconnection", function(sck, c)
                sck:close();
                print("disconnect");
                print(global_sleep);
                if (tonumber(global_sleep)>0) then
                    node.dsleep(global_sleep);
                end;
            end );
        c:connect(tcp_port, tcp_addr); --everything else is event driven from this moment
        tmr.stop(0)
    end
end)