nodeMCU with a power relay used to operate a garage door via wifi
I recently bought some knock-off hiletgo nodeMCU modules. They are basically a powerful arduino with wifi. The possibilities are only limited by your imagination. I think these are especially useful for home automation systems. I made a web-based garage door opener using a nodeMCU and a power relay module. To operate a garage door, you simply short a pair of wires together for two seconds and release.
Wifi is the nodeMCU’s power. Using the available libraries, it can act as a web server, send / receive tcp and udp packets, and change the states of it’s gpio pins based on wireless input. My wife hates when I deploy an experiment in the house that has a rat’s nest of wires hanging out or some unfinished prototype that resembles a road side bomb. I knew that if I wanted to leave this in place permanently, it had to look decent.
garage door opener using nodeMCU in a project enclosure
Using this set up, the whole family can have a garage door opener on their phone. Or, I could open it remotely over the internet if I wanted to let someone into the house.
My set up doesn’t merely operate the door, I have a reed switch connected to an input on the nodeMCU that reports to my home made alarm monitoring system via udp packets whether the door is open or closed.
alarm monitoring station using a pine64
My alarm monitoring station deserves it’s own post. It has a pine64 for brains and uses most of my pine64 modules that I have posted on cpan. An mcp23008 drives the LED indications, it runs an Apache web server that I can access over the internet to monitor alarm conditions, and receives udp updates from alarm modules such as the garage door opener. Locally, it displays alarm conditions on a two row MAX7219 LED display using my PINE64::MAX7219 module, also on cpan.
screenshot of web interface to nodeMCU based garage door opener home automation system
I use port forwarding on my ISP cable modem to control this and my other alarm modules over the internet.
A PCB I designed in KiCad and had manufactured by JLCPCB
I normally fabricate my projects on prototype printed circuit boards where I make the circuits with jumper wires. It is error prone and very time consuming. I recently watched a tutorial series made by digi-key on how to design you own PCB’s using KiCad and order them from a PCB manufacturer.
My first project is a board designed to power an Arduino Nano, and break out several analog and digital inputs. One 12-pin header is specifically designed to break out a 16X2 LCD. I designed it to accept a MeanWell IRM-10-5 10 watt 5VDC AC-DC converter to power the arduino and any peripherals. I am really pleased with how it turned out.
Here is the KiCad schematic. Here is my design in PCBNEW in the early stages before I drew the traces on the board.
The tutorial suggested that I go with OSHPARK to produce my boards. I uploaded my gerber files and the showed me proofs of my design. They quoted me $48 for three boards. I checked around and found that JLCPCB would make five for $2. I thought it was too good to be true, but after shipping, I only paid $13 for 5 boards. They are very high quality, and I only had to wait about 10 days. That’s even cheaper than the prototype PCB’s I had been using. I was able to solder all the components on the board in a few short minutes as opposed to hours. This is definitely the way I will go from now on.
My latest module to upload to CPAN is a perl-based driver for the MCP23008 GPIO extender. This chip is very handy when you have ran out of GPIO pins on your single board computer and gives you 8 more digital I/O’s. This has come in handy for me on several occasions when I used an LCD for a project that took too many of my digital pins.
The driver works as you would expect: you can make any combination of inputs or outputs, and read in the state of the inputs. This is an i2c device that defaults to an address of 0x20. You can adjust the address by putting high / low values on the address lines A0 – A2 for a possible 8 different chips on a single i2c bus.
Below is a simple implementation of the module configured as an input and then as an output from the synopsis on CPAN ->
The methods basically use Device::I2C to manipulate the chip’s internal registers. Register 0x00 is the I/O direction register. This is an 8-bit register with each bit position representing a digital pin. Valid values are 0 – 255 where all pins default to outputs. So, to make pin 6 an input, you would call the set_direction() method like so:
set_direction(64);
The MCP23008 has internal 100K Ohm pull up resistors when pins are configured as inputs. This can save you room on your board by not having to build external pull up resistors yourself. This is configured in the GPPU (GPIO Pull-up resistor register) 0x06. The code below enables all 8 of the internal pull up resistors.
enable_pullup(255);
I also implemented the I/O polarity feature. With this enabled, the chip will give you the opposite polarity of the current state of the pin. The function call below reverses the polarity of pin 5.
set_polarity(32);
The main methods of the module are read_pin() and write_pin(). They work as you would expect. It reads / writes values to the GPIO port register 0x09.
$gpext->read_pin(4); $gpext->write_pin(7, 1);
Here is a description of the GPIO port register straight out of the datasheet:
I didn’t implement all of the features of the MCP23008, but enough to make them useful for most projects. Here is the complete source code for the module:
1 #!/usr/bin/perl
2 use strict;
3 use Device::I2C;
4 use IO::Handle;
5 use Fcntl;
6 package PINE64::MCP23008;
7 our $VERSION = '0.9';
8 #global vars
9 my ($i2cbus, $addr, $gpext);
10 my $gpregval = 0; #init gpio register value to 0
11 my @pin_nums = (1,2,4,8,16,32,64,128);
12 sub new{
13 my $class = shift;
14 my $self = bless {}, $class;
15 #first arg is device address
16 $addr = $_[0];
17 #second arg i2c bus; optional
18 $i2cbus = $_[1];
19
20 if($i2cbus eq ''){
21 $i2cbus = '/dev/i2c-0';
22 }#end if
23
24 $gpext = Device::I2C->new($i2cbus, "r+");
25 #init i2c device
26 $gpext->checkDevice($addr);
27 $gpext->selectDevice($addr);
28 #init gp register val to all off
29 $gpregval = 0;
30 return $self;
31 }#end new
32 sub set_direction{
33 #sets value of the IO direction register
34 #ie 255 makes all input; 0 makes all output
35 my $direction = $_[1];
36 $gpext->writeByteData(0x00, $direction);
37 }#end set_direction
38 sub enable_pullup{
39 #when a pin is configured as an input
40 #you can enable internal 100K pull-up
41 #resistors by writing to the GPPU register
42 #0-255 are valid values: 0 - all disabled
43 #255 - all enabled
44
45 my $en_gppu = $_[1];
46 $gpext->writeByteData(0x06, $en_gppu);
47 }#end enable_pullup
48 sub set_polarity{
49 #sets the polarity of the gpio pins;
50 #0 is normal polarity
51 #255 is all pins reversed
52
53 my $io_pol = $_[1];
54 $gpext->writeByteData(0x01, $io_pol);
55 }#end set_polarity
56 sub write_pin{
57 my $ind = $_[1];
58 my $iox = $pin_nums[$ind];
59 #1 or 0
60 my $val = $_[2];
61 if($val == 1){
62 $gpregval+=$iox;
63 }#end if
64 if($val == 0){
65 $gpregval-=$iox;
66 }#end if
67 $gpext->writeByteData(0x09, $gpregval);
68 }#end write_pin
69 sub read_pin{
70 my $ind = $_[1];
71 my $iox = $pin_nums[$ind];
72 my $pinval = 0;
73 #read GPIO register
74 my $regval = $gpext->readByteData(0x09);
75 #ensure 8 binary places are displayed
76 my $binout = sprintf("%08b", $regval);
77 #parse eight binary digits into an array
78 my @pinvals = split(//, $binout);
79
80 #reverse array to match pin #'s
81 @pinvals = reverse(@pinvals);
82 #value of pin is index of $pinvals
83 $pinval = $pinvals[$ind];
84 return $pinval;
85 }#end read_pin
86 1;
87 __END__
One of the drawbacks, in my opinion, of the raspberry pi and the pine64 A+ is the lack of any analog inputs. To read an analog signal requires an external analog to digital converter chip, and will tie up several of your GPIO lines. My new module has routines to read values on an MCP3004 or MCP3008 10-bit analog to digital converter.
Here is some sample code ->
usePINE64::MCP300x;my$adc= PINE64::MCP300x->new(10,12,11,13); #5 bits because the first is the start bitmy@ch0= (1,1,0,0,0);for(my$s=0;$s<200;$s++){my($reading, $binval, $voltage) = $adc->read300x(\@ch0, 50, 5.01);$voltage= sprintf("%.3f", $voltage);print"binval: $binval\tvoltage: $voltage vdc\n";usleep(500000);}#end for
I have had another cpan module published! This one is a driver for a MAX7219 8-digit LED display on the PINE64 single board computer. The video above is a demo of some of the module’s capabilities. It is implemented as bit-banged SPI using the perl programming language.
A very easy to use display, it only uses 3 GPIO pins, and could be used to output the value of sensors, or simple menus for projects.
Installed lead-lag controllerlead-lag controller using arduino nano
My office is in a telecommunications facility full of routers, radios, fiber-optic equipment etc. Reliable environmental controls are essential as this equipment will fail in extreme heat. I have two 20,000 BTU AC units in my office that run on 250VAC. I created this lead-lag controller to turn on the lead AC unit based on the temperature setting, and if the lead unit has run for six minutes without cooling to the setting, turns on the lag unit as well until the temperature setting is reached.
arduino lead-lag hvac controller
I have the programming down and the device built and installed. Initial tests work as designed. I am using some 10A 250VAC power relays. With the unit running on high fan speed and the compressor going the AC units draw about 5 amps; well within the rage of the relays.
the lead / lag controller turns on/off the 250VAC outlets of the air conditioner units
Here is the code ->
// include the library code:
#include <LiquidCrystal.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include "RTClib.h"
RTC_DS3231 rtc;
// Sensor input pin
#define DATA_PIN 2
// How many bits to use for temperature values: 9, 10, 11 or 12
#define SENSOR_RESOLUTION 9
// Index of sensors connected to data pin, default: 0
#define SENSOR_INDEX 0
OneWire oneWire(DATA_PIN);
DallasTemperature sensors(&oneWire);
DeviceAddress sensorDeviceAddress;
// initialize the library by associating any needed LCD interface pin
// with the arduino pin number it is connected to
const int rs = 12, en = 11, d4 = 10, d5 = 9, d6 = 8, d7 = 7;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
int sample_num = 0;
//thermostat setting; init to 72
int setting = 70;
//heat or cool mode
char mode = 'C';
//relay pins
const int ac1Pin = 5;
const int ac2Pin = 4;
//const int heatPin = 6;
//control buttons
const int modePin = 3;
const int decrPin = 6;
const int incrPin = 0;
//AC ON/OFF flags
int ac1_f = 0;
int ac2_f = 0;
int heat_f = 0;
//on timers
int ac1_timer = 0;
int ac2_timer = 0;
//RTC stuff
char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
void setup() {
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
Serial.begin(9600);
//AC1 relay
pinMode(ac1Pin, OUTPUT);
pinMode(ac2Pin, OUTPUT);
//pinMode(heatPin, OUTPUT);
//set AC1 off
digitalWrite(ac1Pin, LOW);
digitalWrite(ac2Pin, LOW);
//digitalWrite(heatPin, LOW);
//control buttons
pinMode(modePin, INPUT);
pinMode(decrPin, INPUT);
pinMode(incrPin, INPUT);
sensors.begin();
sensors.getAddress(sensorDeviceAddress, 0);
sensors.setResolution(sensorDeviceAddress, SENSOR_RESOLUTION);
if (! rtc.begin()) {
Serial.println("Couldn't find RTC");
while (1);
}
if (rtc.lostPower()) {
Serial.println("RTC lost power, lets set the time!");
// following line sets the RTC to the date & time this sketch was compiled
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// This line sets the RTC with an explicit date & time, for example to set
// January 21, 2014 at 3am you would call:
// rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
}
}
//array to hold 12 readings; 5-per min
float t_readings[12];
//sum of readings
float sum = 0.0;
//avg of readings
float average = 0.0;
void loop() {
// set the cursor to (0,0):
lcd.setCursor(0, 0);
//reset sum and avg on each iteration
sum = 0;
average = 0;
//12 sample loop; takes 1 min
for(int x=0;x<12; x++){
lcd.setCursor(0, 0);
sensors.requestTemperatures();
// Measurement may take up to 750ms
float temperatureInCelsius = sensors.getTempCByIndex(SENSOR_INDEX);
float temperatureInFahrenheit = sensors.getTempFByIndex(SENSOR_INDEX);
Serial.print("Temperature: ");
Serial.print(temperatureInCelsius, 1);
Serial.print(" Celsius, ");
Serial.print(temperatureInFahrenheit, 1);
Serial.print(" x: ");
Serial.print( x );
Serial.println(" Fahrenheit");
//push reading into array
t_readings[x] = temperatureInFahrenheit;
lcd.print(temperatureInFahrenheit);
lcd.print("F");
//lcd.setCursor(0, 1);
//lcd.print(sample_num);
//sample_num++;
if(digitalRead(modePin) == LOW){
Serial.println("MODE BUTTON PRESSED! ENTERING SETUP....");
setTemp();
}//end if mode pressed
//print setting to lcd row 2
lcd.setCursor(0, 1);
lcd.print("Setting: ");
lcd.print(setting);
//print mode
lcd.setCursor(15,0);
lcd.print(mode);
delay(5000); //sleep 5 sec
}//end for loop 12 sample
//average samples
for(int i=0; i<12; i++){
sum = sum + t_readings[i];
}//end for avg
Serial.print("sum: ");
Serial.println(sum);
average = sum / 12;
Serial.print("avg: ");
Serial.println(average);
//AC relay control
if(average > setting && mode == 'C'){
digitalWrite(ac1Pin, HIGH);
//flag logic
if(ac1_f == 0){ ac1_f = 1;}
//timer logic
ac1_timer += 60; //60 because of the 12 samples/min cycle
Serial.print("AC UNIT 1 ON ");
Serial.print(ac1_timer);
Serial.print(" seconds ");
if(ac1_timer > 60){//turn on ac2 after 30 minutes
if(ac2_f == 0){
ac2_f = 1;
digitalWrite(ac2Pin, HIGH);
}
else{
//increment ac2_timer
ac2_timer += 60;
}
Serial.print("AC UNIT 2 ON ");
Serial.print(ac2_timer);
Serial.print(" seconds ");
}//end if turn on AC2
//poll RTC for time
DateTime now = rtc.now();
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(" ");
Serial.print(" (");
Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
Serial.print(") ");
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.println(now.second(), DEC);
}//end if
if(average < setting && mode == 'C'){
//turn AC1 OFF, reset timer & flags
digitalWrite(ac1Pin, LOW);
ac1_f = 0;
ac1_timer = 0;
Serial.print("AC UNIT 1 OFF ");
//if on, turn off AC2
if(ac2_f == 1){
digitalWrite(ac2Pin, LOW);
ac2_f = 0;
ac2_timer = 0;
Serial.print("AC UNIT 2 OFF ");
}//end if
//poll RTC for time
DateTime now = rtc.now();
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(" ");
Serial.print(" (");
Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
Serial.print(") ");
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.println(now.second(), DEC);
}//end if
delay(2000);
}
void setTemp(){
int su_flag = 1;
int mode_flag = 1;
Serial.println("SETUP MODE ->");
lcd.begin(16, 2);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Set temp: ");
lcd.print(setting);
delay(1000);
//lcd.clear();
while(su_flag == 1){
if(digitalRead(decrPin) == LOW){
setting--;
lcd.setCursor(10, 0);
lcd.print(setting);
}//end if exit setup
if(digitalRead(incrPin) == LOW){
setting++;
lcd.setCursor(10, 0);
lcd.print(setting);
}//end increment temp
if(digitalRead(modePin) == LOW){
//exit setup
su_flag = 0;
}//end exit setup mode
delay(250);
}//end while
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Set mode: ");
lcd.print(mode);
delay(1500);
while(mode_flag == 1){
if(digitalRead(decrPin) == LOW){
mode = 'C';
lcd.setCursor(10, 0);
lcd.print(mode);
}//end if
if(digitalRead(incrPin) == LOW){
mode = 'H';
lcd.setCursor(10, 0);
lcd.print(mode);
}//end if heat mode
if(digitalRead(modePin) == LOW){
mode_flag = 0;
}//end if exit mode set
delay(250);
}//end while set mode
lcd.clear();
}//end setTemp function
raspberry pi zero interfacing with 18B20 1-wire temperature sensor
I am pretty impressed with the responsiveness and accuracy of the 1-wire 18B20 temperature sensor. This is the first 1-wire sensor I have ever used. To get it going on the pi zero, I had to enable the 1-wire protocol via raspi-config, and edit the /boot/config.txt file to tell it what pin to use. Like gpio, 1-wire has a file system interface, which makes perl a great tool to interface to 1-wire devices.
Interestingly, each 1-wire device has a unique serial number, so it is possible to have literally thousands of 1-wire devices on a single bus; certainly far more than i2c allows. Here is the command line output of a manual reading from the 18B20:
file system interface on pi zero for a 1-wire 18B20 temperature sensor
The part of the output that says t=26500 is the temperature reading in celsius i.e. 26.500 or 79.7F.
Here is my perl code for interfacing with the 18B20 ->
#!/usr/bin/perl -w
use strict;
=pod
reads a 1wire 18B20
include this in your script,
returns fahrenheit
=cut
#my $temp = read_18b20();
#print "temp: $temp\n";
sub read_18b20{
`ls /sys/bus/w1/devices > 1w.txt`;
open OW, "1w.txt" or die $!;
my $temp_sensor = '';
while(<OW>){
if($_ =~ /(28-............)/){
#print "serial number: $1\n";
$temp_sensor = $1;
}#end if
}#end while
close OW;
my ($tempC, $tempF) = (0,0);
`cat /sys/bus/w1/devices/$temp_sensor/w1_slave > 1w_reading.txt`;
open RD, "1w_reading.txt" or die $!;
while(<RD>){
if($_ =~ /t=(\d\d)(\d\d\d)/){
$tempC = $1.".".$2;
#print "celsius: $tempC\n";
}#end if
}#end while
close RD;
$tempF = $tempC * (1.8) + 32;
#print "farenheit: $tempF\n";
return $tempF;
}#end read_18b20
1;
Here is sample output from a script that samples the sensor every 2 seconds. I breathed on the 18B20 to demonstrate the responsiveness.
samples from a 1-wire 18B20 temperature sensor using perl interface
I admit I know little about pulse width modulation, and have limited experience with robotics or servos. I’ve had a futaba s3003 servo in my toolbox for some time, and done nothing with it. On a lazy day when I had some down time, I decided to get it out and do some experimentation.
I’ve been doing some labor intensive projects at work lately, and while hard work is very rewarding, it takes a lot of physical and mental energy and crowds out innovation. Just out of boredom, I burned the most basic servo script example provided by the arduino IDE into a nano clone I recently bought, and analyzed it with pulseview to better understand pwm.
close up of PWM controlling a futaba s3003 servo
Above is a trace of the output of the default arduino ide servo script with me tuning a 10K pot. In this example script, the position of the servo corresponds to the position of the potentiometer. Just at a glance, you can see pulses bunched up at different intervals. What I empirically observed is that when the servo was at position 1, the pulse width was about 0.55ms in duration at the standard 50Hz required by the servo. This pulse was constant, and the servo was stationary. If the pulse width changes at all, it changes position proportionally.
The futaba s3003 has 180 degrees of rotation. At position 3 (180 degrees) the constant pulse width is about 2.4msec. as you can see in the graphic below:
This simple analysis has demystified pwm somewhat for me. Next, I hope to experiment with some pwm on the raspberry pi using perl.
receive signal strength indication vs. relative humidity data logger
I finally got around to deploying my rssi / temperature / relative humidity data logger and recorded nearly 6,000 samples of each metric over a five day period. This radio system operates in the ISM band, and is susceptible to propagation issues due to atmospheric conditions, especially humidity. The logic board provides a terminal that outputs a DC voltage that represents the rssi in dBm. My data logger uses a raspberry pi zero w, and reads the rssi voltage with an MCP3004 analog to digital converter using a bit-banged driver I wrote in perl. Each reading is timestamped using time derived from a DS3231 i2c real-time clock chip.
I use a HTU21 temperature / relative humidity sensor break out board from adafruit. This chip is MUCH MORE STABLE & RELIABLE than the DHT22 I was originally using.
temperature vs. relative humidity data capture
For starters, here is the temperature in fahrenheit (red) vs. the relative humidity (green). Predictably, when the temperature rises, the relative humidity goes down and vice versa.
rssi level vs. relative humidity at 900MHz
Now here is the relative humidity vs. the rssi. This radio is about 20 miles away from the transmitter over flat terrain with an output of 37dBm (5 watts), and has an average rssi of about -90dBm. You can see from the above graph that at about sample 4,000 when the relative humidity takes a noticeable dip that the rssi has a corresponding, albeit small, increase: precisely what we would expect to see.
Nothing surprising here. It just proves the effectiveness of my data logger device. It would be useful in situations where an RF path is going up and down due to atmospheric conditions or other obstructions, and real-time data would be useful in diagnosing problems and coming up with solutions like a higher gain antenna or increasing height.
I am working on a data logging project based on the raspberry pi zero. NOT a pi zero w, just the pi zero. Adding software packages to the pi zero is a little more difficult, because it has no wifi or ethernet connection. I have been taking the sd card out ant putting it in a pi 3b+ to get packages that I need then putting it back in the pi zero. I have tried and failed to use minicom and zmodem to transfer deb files over UART.
I found this great tutorial that explains how to ssh into a pi via the usb port on the pi. I did everything it said and was still not able to connect to the pi. I did an ifconfig on the pi, and saw that it had a APIPA on it’s usb network interface like so:
usb network interface on pi zero
I had a usb network interface appear on my laptop, but no ipv4 address:
usb network interface on ubuntu machine
I finally got it to work by issuing an ifconfig command to my laptop’s usb interface and assigning an IP address in the same class B subnet :
ifconfig on usb network interface APIPA
Another benefit to a standard pi zero having ip connectivity via usb is that you can use piscope for troubleshooting. Here is piscope examining i2c frames polling an HTU21D relative humidity sensor:
examining an HTU21D with piscope on a pi zero with usb network interface