nodeMCU garage door opener

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.

nodemcu garage door opener home automation
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.

JLCPCB and KiCad

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.

Music Minus One: Anton Rubinstein Piano Concerto No. 4 D minor Op. 70

I have never seriously attempted to play a piano concerto before. I just assumed they were all too difficult for me, and that I would never be able to play with an orchestra anyway, so what’s the point? All that may still be true, however, I recently discovered the Music Minus One series that makes orchestra-only recordings.

Before I started seriously practicing, I printed a two-piano version of Anton Rubinstein’s 4th piano concerto in D minor Op. 70 on IMSLP and toyed around with it for several weeks, as I routinely do for pieces that I may or may not ever work on. I just wanted to play that big, powerful main riff on the opening, mostly. I kept fooling around with it and without too much effort, had learned most of the first movement, albeit very badly.

One evening, I was playing along with a professional recording that was way too fast for me, but I had so much fun when I could keep up. That’s when I searched and found the MMO recording.

The MMO recording comes with two CD’s: both have an awesome complete performance, and then a orchestra-only performance. One is at full speed, and the other claims to be 20% slower. I am practicing with the slower version.

The first few times playing along with the orchestra track revealed some very bad timing issues in my playing. Also, the recording was too quiet at times that I could not hear it well enough to play along even when I wore headphones at full blast and played quietly. As time progressed, It became much easier to keep up even when there are long silences in the recording. I used audacity to amplify the quiet parts of the track by up to 9dB and it is working well for me.

At this point, I have been seriously practicing for about 8hrs a week for the last three months. My goal is to get it polished enough to perform in a concert at my church.

PINE64::MCP23008 Module on CPAN

pinout ot the MCP23008 GPIO extender chip
pinout ot the MCP23008 GPIO extender chip

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__

Recognized on Perl.com!

My latest cpan modules were mentioned in a recent article on Perl.com! It has been very rewarding to be able to contribute to cpan after so many years as a consumer. It is even more awesome to have some of my analog to digital converter modules (PINE64::MCP300x & PINE64::MCP3208) mentioned in the hardware section of this recent blog post.

PINE64::MCP300x CPAN Module

My MCP300x module on CPAN for the PINE64A+ boards

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 ->

use PINE64::MCP300x; 
my $adc = PINE64::MCP300x->new(10,12,11,13); 
#5 bits because the first is the start bit
my @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

Here is the output of the script ->

PINE64::MAX7219

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.

PINE64::GPIO module published on CPAN

my first perl module published on CPAN

One of my personal goals this year was to publish some of my work that uses perl to experiment with single board computers on CPAN (the Comprehensive Perl Archive Network). I have used perl to make windows applications, database applications, web applications, text processing utilities, inventions….. you name it. However, it is not widely used for single board computers like the raspberry pi. There are a lot of reasons for this. Perl is no longer the cool programming language say, like Python is. Programming in perl is almost an anachronism and dates you. It is relatively slow. Even so, perl is incredibly versatile and powerful and has a devoted following of some of the most brilliant people. Perl isn’t going anywhere.

I have been a consumer of CPAN for nearly 10 years. CPAN is a code repository where programmers share their code so you don’t have to re-invent the wheel in your programs if someone else has already figured out how to implement what you are trying to do.

Steve Bertrand and others have published a ton of useful modules for the raspberry pi family of SBC’s on CPAN. Dude even published a book on indiegogo on pi projects using perl. I am not near as smart as Mr. Bertrand, so I wanted to stay away from publishing my pi modules on CPAN. The idea is to do something that hasn’t been done before. No one like in the whole world seems to be using perl on the Pine64 board (if you google, I am virtually the only person doing it), so I decided to post my modules for the PineA64+.

To publish on CPAN, you have to have a PAUSE account (Perl Authors Upload Server). You have to be ‘in the club’. You basically have to apply for membership stating what you intend to offer. I also gave them the address of this site so they could see some of my work. I didn’t hear anything back for nearly six weeks. I assumed they looked at my projects and declined to grant me access. I finally did get an account and proceeded to package my modules to their requirements and uploaded my first module: a script that controls the GPIO pins on the PineA64 board’s Pi-2 bus. I consider it a tremendous honor to have been accepted.

I have a technology degree with some graduate work in computer science. I work in telecom, however, not as a programmer. Programming, though, is my secret weapon. Paired with the knowledge of electronics hardware and fabrication, I can create just about anything I can think up. It is a thrill to finally get published on CPAN. I will have many more modules for the PineA64 to follow for various sensors, analog to digital converters, displays, etc.

arduino based hvac lead-lag controller

arduino lead-lag hvac control
Installed lead-lag controller
arduino lead-lag hvac control
arduino lead-lag hvac control
arduino hvac lead-lag controller
lead-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
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

Beethoven Piano Sonata No. 4 E-flat Major (Grand Sonata)

First Movement

2020 is the 250th anniversary of Beethoven’s birth. Mrinda and I were supposed to attend an all Beethoven concert celebrating this anniversary at the Germantown performing arts center, with the main event being a performance of Beethoven’s first piano concerto and Valery Kuleshov as soloist. It was cancelled due to COVID-19.

However, in honor of this important date, I wanted to learn some Beethoven sonatas that I had never worked on or even heard. I went to the IMSLP and listened to all 32 of them and found a few that I was interested in.

No. 4 in E-flat major is really fun to play. Andras Schiff has a brilliant lecture on this piece here. I found it interesting that this is the longest sonata next to the Hammerklavier. I was totally unfamiliar with this sonata, so it was a great surprise.