Arduino GPS-based Lap Timer Revisited

GPS-based lap timing code is located here. The source code provided is an MSVC program used for testing purposes. This is simply the basic algorithms needed to function. Very little error checking is performed by the program. It can easily be modified to work on a laptop using a GPS connected through the USB. With minimal modification, it could also be adapted for use on a microcontroller. A text-based file of GPS RMC sentences recorded from several laps of the Portland International Raceway circuit can be used with the program and is located in my github repository.

This is a work in progress [updated on 1/13/2020].

The main loop of the program looks like this:

	// Main gps string processing loop.
	while (1)
	{
		if (!GetRMCSentence(port, tokens))
		{
			std::cout << error.GetDescription() << std::endl;
			continue;
		}

		// Previous position gps time stamp.
		float prevTimeStamp = timeStamp;
		
		// Confirm sentence is sequential.
		timeStamp = atof(tokens[RMC_TIME]);
		if (!Equal(timeStamp, prevTimeStamp + GPS_UPDATE_PERIOD) && !Equal(timeStamp, prevTimeStamp + 40. + GPS_UPDATE_PERIOD))
		{
			error.SetError(err::ID::TIME_STAMP);
			std::cout << error.GetDescription() << std::endl;
			continue;
		}

		// Get current track position (lat, long).
		if (tokens[RMC_LATITUDE] != nullptr || tokens[RMC_LONGITUDE] != nullptr)
		{
			char temp[12];

			GeoCopy(tokens[RMC_LATITUDE], temp, LATITUDE);
			track.p1.x = atof(temp);
			GeoCopy(tokens[RMC_LONGITUDE], temp, LONGITUDE);
			track.p1.y = atof(temp);
		}
		else
			continue;

		// Ignore gps sentences for 1 second after crossing start/finish.
		if (hzCounter < GPS_UPDATE_FREQUENCY)
		{
			hzCounter++;
			// Prepare for next iteration.
			track.p0.x = track.p1.x;
			track.p0.y = track.p1.y;
			continue;
		}
		
		// Heading sanity check & check if crossed start/finish line?
		if (Within30(startHeading, (uint16_t)atol(tokens[RMC_TRACK])) && LineIntersection(track))
		{
			point_t intersectPoint;

			// Calculate track/start line intersection point.
			IntersectPoint(track.p0, track.p1, &intersectPoint);

			// Overall length of this track segment.
			float totDist = Distance(track.p0, track.p1);

			// Length from start line intersection point to track segment end point.
			float segDist = Distance(intersectPoint, track.p1);

			// Calculate startline crossing time for this and next lap.
			float xTime = timeStamp - (GPS_UPDATE_PERIOD * (segDist / totDist));
			lapData[numLaps].setStop(xTime);
			lapData[numLaps + 1].setStart(xTime);

			// Determine current lap stats.
			DisplayTime(numLaps + 1, lapData[numLaps].getTime());
			if (numLaps > 0)
				DisplayTime(bestTime.first + 1, bestTime.second);

			// Is this lap a new best?
			if (numLaps == 0 || lapData[numLaps].getTime() < bestTime.second)
			{
				// Announce new fast lap.
				std::cout << " << Fast Lap";
				bestTime = std::make_pair(numLaps, lapData[numLaps].getTime());
			}
			std::cout << "\n";

			// Increment counters.
			numLaps++;
			hzCounter = 1;
		}

		// Prepare for next iteration.
		track.p0.x = track.p1.x;
		track.p0.y = track.p1.y;
	}
Posted in arduino, c | Tagged , , | Leave a comment

Arduino BBQ Fan Temperature Controller

Commercially sourced BBQ temperature controllers are expensive. They start at about US $200 and average well above $300. Most include WIFI interfaces too. So I thought I could do better. This is the first step in making a BBQ temperature controller. The next step will be to add a simple flue control. Eventually, I want to add WIFI control to the device.

This project uses the following components. Most of my parts were sourced off ebay and came from China.

  • Arduino
  • 12v fan
  • 12v power supply
  • L298N motor control module
  • MAX6675 k-type thermocouple module
  • TM-1637 4-digit 7-segment display
  • 2 switches

Obviously, the fan is one of the most important items in how well the system operates. Fans are rated by their airflow, typically given in Cubic Feet per Minute (CFM). Here is an example of a nominally-sized, fan with a decent rating of 40 CFM at 12 volts.

So far, the system simply allows the user to select a “set-point” temperature and stores this value in EEPROM. An attached MAX6675 k-type thermocouple (0-500 degree C range) is used to check the temperature inside the BBQ. The variable fan motor speed is adjusted depending upon the temperature difference between the actual and set-point values. Signals sent from the Arduino to the L298N module are the commands to set a specific fan speed. An additional benefit to using an L298N module is it has a 5V output capable of supplying the other components. The two buttons are used to display and adjust the “set-point” temperature. A TM-1637 4-digit, 7-segment display is incorporated to show either temperatures.

Finer temperature control will be achieved through use of a flue door. This will help to prevent temperature overshoots and allow for the full range of temperature throttling. I envision using a flue door operated via a servo and possibly incorporating software PID control if necessary.

The complete system can be viewed below:

Here is a matrix showing all of the interconnections between modules. It’s really not as difficult as it looks:

The project consumes 10 digital pins on the Arduino. And while I used an old Atmega168 based Arduino, a 328, Uno or Leonardo would work equally as well. The Arduino firmware is very simple:

/*
 * Temperature Controlled BBQ Variable Fan
 * James M. Eli
 * 8/20/2019
 * Version 1.0
 * 
 * Components:
 *   Arduino (Atmega168)
 *   MAX6675 K-Type Thermocouple
 *   TM1637 7-segment 4-digit Display
 *   L298N Motor Contorller Module
 *   12V Fan
 *   12V Power Supply
 *   
 * Sketch uses 4096 bytes (28%) of program storage space. 
 * Maximum is 14336 bytes.
 * Global variables use 47 bytes (4%) of dynamic memory, 
 * leaving 977 bytes for local variables. 
 * Maximum is 1024 bytes.
 */
#include "TM1637Display.h" // display module
#include "max6675.h"       // temperature module
#include "EEPROM.h"        // eeprom access

// Program states.
constexpr uint8_t NO_STATE{ 0 };
constexpr uint8_t INC_TEMP{ 1 };
constexpr uint8_t DEC_TEMP{ 2 };
constexpr uint8_t SET_TEMP{ 3 };

// TM1637 display module connection pins.
constexpr uint8_t TM1637_CLK { 2 };   // PD2
constexpr uint8_t TM1637_DIO { 3 };   // PD3
// MAX6675 temperature module connection pins.
constexpr uint8_t MAX6675_SO { 8 };   // PB0
constexpr uint8_t MAX6675_CS { 9 };   // PB1
constexpr uint8_t MAX6675_CLK { 10 }; // PB2
// L298N module connection pins.
constexpr uint8_t L298N_ENA { 5 };    // PD5, l298n pin #7 (motor 1 enable, remove jumper)
constexpr uint8_t L298N_IN1 { 7 };    // PD7, l298n pin #8 (IN1)
constexpr uint8_t L298N_IN2 { 6 };    // PD6, l298n pin #9 (IN2)
// Buttons (2).
constexpr uint8_t INC_BUTTON { 11 };  // PB3
constexpr uint8_t DEC_BUTTON { 12 };  // PB4

// Fan speed temperature thresholds (degrees F).
constexpr int16_t TEMP_THRESHOLD[4] = { 7, 16, 33, 65 };
// Fan speeds voltages (0-255, where 255=100%).
constexpr uint8_t FAN_SPEED[4] = { 63, 127, 191, 255 };

// Temperature value change per each increase/decrease.
constexpr int16_t TEMP_STEP { 10 };
// Min, max and default temperature settings.
constexpr int16_t MIN_TEMP { 200 };
constexpr int16_t MAX_TEMP { 500 };
constexpr int16_t DEFAULT_TEMP { 350 };

// Eeprom address (stores set point temperature).
#define EEPROM_ADDRESS 0 

// Display timer.
uint32_t timer;
// Set point temperature & actual temperature (as read from MAX6675).
int16_t setTemp;
int16_t actTemp;
// Pointer to select temperature for display.
int16_t *temp = &actTemp;

// Instantiate display & temperature module objects.
TM1637Display disp(TM1637_CLK, TM1637_DIO);
MAX6675 tc(MAX6675_CLK, MAX6675_CS, MAX6675_SO);

// Read/write word to/from EEPROM.
int16_t eepromRead16(uint16_t address) 
{
  int16_t value = word(EEPROM.read(address), EEPROM.read(address + 1));
  return value;
} 
void eepromWrite16(uint16_t address, int16_t value) 
{
  EEPROM.write(address, highByte(value));
  EEPROM.write(address + 1, lowByte(value));
}

// Retrieve temperature from eeprom.
int16_t getEepromTemp(void) 
{
  int16_t temp;

  // Get temp from eeprom memory.
  temp = eepromRead16((uint16_t)EEPROM_ADDRESS);

  // Validate temp in range & multiple of step value.
  temp = (temp / TEMP_STEP) * TEMP_STEP;
  if (temp < MIN_TEMP || temp > MAX_TEMP)
    temp = DEFAULT_TEMP;
  
  // Save it.
  eepromWrite16((uint16_t)EEPROM_ADDRESS, (int16_t)temp);
  return temp;
}

// Store temperature in eeprom.
void setEepromTemp(int16_t temp) 
{
  // Validate temp in range & multiple of step value.
  temp = (temp / TEMP_STEP) * TEMP_STEP;
  if (temp < MIN_TEMP || temp > MAX_TEMP)
    temp = DEFAULT_TEMP; 
  
  // Save it.
  eepromWrite16((uint16_t)EEPROM_ADDRESS, (int16_t)temp);  
}

// Poll buttons.
uint8_t checkButtons() 
{ 
  // Both buttons down?
  if ( !(PINB&(1<<PORTB3)) && !(PINB&(1<<PORTB4)) ) 
    return SET_TEMP;
  
  // Increment button down?
  else if ( !(PINB&(1<<PORTB3)) ) 
    return INC_TEMP; 
  
  // Decrement button down?
  else if ( !(PINB&(1<<PORTB4)) ) 
    return DEC_TEMP; 
  
  return NO_STATE;
}

void setup() 
{
  // L298N motor controller pins.
  pinMode(L298N_ENA, OUTPUT);
  pinMode(L298N_IN1, OUTPUT);
  pinMode(L298N_IN2, OUTPUT);
  
  // TM1637 display module connection pins.
  pinMode(TM1637_CLK, OUTPUT);
  pinMode(TM1637_DIO, OUTPUT);
  
  // MAX6675 temperature module connection pins.
  pinMode(MAX6675_SO, OUTPUT);
  pinMode(MAX6675_CS, OUTPUT);
  pinMode(MAX6675_CLK, OUTPUT);
  
  // Activate internal pull-ups on button pins.
  pinMode(INC_BUTTON, INPUT);
  pinMode(INC_BUTTON, INPUT_PULLUP);
  pinMode(DEC_BUTTON, INPUT);
  pinMode(DEC_BUTTON, INPUT_PULLUP);

  setFan(0);                 // Turn fan motor off.
  disp.setBrightness(0);     // Dim down.
  timer = millis();          // Initialize timer.
  setTemp = getEepromTemp(); // Retrieve stored set point temperature.
  delay(500);                // Allow everything to settle.
}

// Update L298N fan motor.
void setFan(uint8_t index) 
{
  if (index == 0) 
  {
    // Turn fan motor off.
    digitalWrite(L298N_IN1, LOW);
    digitalWrite(L298N_IN2, LOW);  
  } 
  else 
  {
    // Turn motor on and adjust speed.
    digitalWrite(L298N_IN1, HIGH);
    digitalWrite(L298N_IN2, LOW);
    analogWrite(L298N_ENA, FAN_SPEED[index]);
  }
}

// Handle TM1637 display details.
void displayTemp(int16_t t) 
{
  disp.setBrightness(0x0f);
  //disp.clear();
  disp.showNumberDec(t, false);  // Show decimal numbers without leading zeros
}

void loop() 
{
  actTemp = (int16_t)tc.readFahrenheit();
  uint8_t button = checkButtons();

  if (button) 
  {
    // Reset display timer.
    timer = millis(); 
    if (temp != &setTemp)
      // Display set point temperature.
      temp = &setTemp;
    else if (button == INC_TEMP)
    {
      // Increment temperature by step amount.
      setTemp += TEMP_STEP;
      if (setTemp > MAX_TEMP)
        setTemp = MIN_TEMP;
      setEepromTemp(setTemp);
    }
    else if (button == DEC_TEMP)
    {
      // decrement temperature by step amount.
      setTemp -= TEMP_STEP;
      if (setTemp < MIN_TEMP)
        setTemp = MAX_TEMP;
      setEepromTemp(setTemp);
    }
  }
  else
  {
    uint8_t speed { 0 };
    // Get temperature difference.
    int16_t dTemp = (int16_t)(setTemp - actTemp);

    // Iterate through temperature thresholds, incrementing fan speed.
    for (int i=0; i<4; i++)
      if (dTemp >= TEMP_THRESHOLD[i])
        speed++;

    setFan(speed);
  }
    
  // Display actual or set point temperature.
  displayTemp((int16_t)*temp);
  
  // A delay is useful for switch debounce.
  if (temp == &setTemp) {
    // Time to change display temperature to actual?
    if (millis() - timer > 5000)
      temp = &actTemp;
    else
    delay(500);
  }
  else
    delay(1000);
}
Posted in arduino | Tagged | 3 Comments

Blynk-ing an ANAVI Light Controller

The ANAVI Light Controller is an open source hardware WiFi development board. As the name suggests it controls 12V RGB LED strip lights and can retrieve data from various I2C sensor modules for temperature, humidity, light and gestures. In this application, I am not using any of these sensors, however, in the future I do intend to experiment with an APDS-9960, primarily for gesture detection.

I want to incorporate LED strip lighting into my existing home automation project. This project is primarily controlled via a Blynk application on my android phone. In this demonstration project, the Blynk application requires 4 slider widgets- one each for the R, G, B colors and an additional one for controlling overall brightness of the LEDs. An additional button widget can be added to provide instant on/off capability and a timer widget could be used for scheduled operation. I further use the functionality of the button to incorporate Alexa voice commands. Use of the timer widget option is not discussed here.

At the heart of the ANAVI Light Controller is an ESP8266-12E. The ESP8266 programming pins are exposed, so the ANAVI board can easily be re-flashed. GPIO0 of ESP8266 is connected to the button called SW1 allowing effortless entry into the programming mode. Furthermore, there is a small red indication LED marked as D1 which could be used for various purposes. In my application, I use this red LED to signal completion of device setup. In order to flash new firmware on ANAVI Light Controller you need a USB to UART serial debug cable and a 12V power supply. Full instruction for re-flashing can be found here.

My replacement firmware code is below. For this project I utilized the PlatformIO IDE with Visual Studio Code, however the Arduino IDE development environment could also be used. In my application, the Alexa voice commands simply allow turning all of the LEDS on and off and is not used for color selection. Also, please note my choice of Blynk virtual pin numbers is purely arbitrary. Good luck.

/*************************************************************************
 * Title: Simple ESP-8266 Wifi RGB LED Strip Light Controller
 * File: main.cpp (esp8266_anavi_rgb_led_controller.ino)
 * Author: James Eli
 * Date: 4/19/2019
 *
 * Program controls an RGB LED strip light via ANAVI light controller device. 
 *
 *************************************************************************
 * 4/19/2019: Migrated to platformio. JME
 *************************************************************************/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <WiFiManager.h> 
#include <ESP8266WebServer.h>
#include <BlynkSimpleEsp8266.h>
#include <WiFiUdp.h>
#include <SimpleTimer.h>
#include "WemoSwitch.h"
#include "WemoManager.h"
#include "CallbackFunction.h"
#include <ArduinoOTA.h>

// Blynk App authentication token, wifi ssid and password.
char auth[] = "...";
char ssid[] = "...";
char pass[] = "...";

// Amazon echo response term & Wifi access point name.
#define ECHO_KEY_WORD "cabinets"

// Maximum brightness.
#define MAX_BRIGHTNESS 255

// Esp8266 pins.
const int ESP8266_RED_PIN   = 12;
const int ESP8266_GREEN_PIN = 13;
const int ESP8266_BLUE_PIN  = 14;
const int ALARM_PIN = 16;
// Blynk virtual pin vs. anavi hw pin assignments.
// Red   = Blynk virtual pin V30 = anavi pin 12
// Green = Blynk virtual pin V31 = anavi pin 13
// Blue  = Blynk virtual pin V32 = anavi pin 14
// V29 = BUTTON
// RGB slider values (0-255).
int red, green, blue;
// Brightness slider (0-255).
int brightness;
// Button status.
int button;
// Connect resync flag.
bool isFirstConnect = true;

WemoManager wemoManager;
WemoSwitch *device = NULL;

void otaStarted() { Serial.println("OTA update started!"); }
void otaFinished() { Serial.println("OTA update finished!"); }
void otaProgress(unsigned int progress, unsigned int total) { Serial.printf("OTA progress: %u%%\r", (progress / (total / 100))); }
void otaError(ota_error_t error) {
    Serial.printf("Error [%u]: ", error);
    switch (error) {
        case OTA_AUTH_ERROR:
            Serial.println("auth failed!");
            break;
        case OTA_BEGIN_ERROR:
            Serial.println("begin failed!");
            break;
        case OTA_CONNECT_ERROR:
            Serial.println("connect failed!");
            break;
        case OTA_RECEIVE_ERROR:
            Serial.println("receive failed!");
            break;
        case OTA_END_ERROR:
            Serial.println("end failed!");
            break;
    }
}

// Turn all LED lines off.
void LEDsOff() 
{
  analogWrite(ESP8266_RED_PIN, 0);
  analogWrite(ESP8266_GREEN_PIN, 0);
  analogWrite(ESP8266_BLUE_PIN, 0);
}

// Turn LEDS on.
void LEDsOn() 
{
  analogWrite(ESP8266_RED_PIN, (red*brightness)/MAX_BRIGHTNESS);
  analogWrite(ESP8266_GREEN_PIN, (green*brightness)/MAX_BRIGHTNESS);
  analogWrite(ESP8266_BLUE_PIN, (blue*brightness)/MAX_BRIGHTNESS);
}

void setup() 
{
  // LED.
  pinMode(ALARM_PIN, OUTPUT);
  digitalWrite(ALARM_PIN, HIGH);

  // WiFiManager intialization.
  WiFiManager wifi;               

  // Init pins.
  pinMode( ESP8266_RED_PIN, OUTPUT );
  pinMode( ESP8266_GREEN_PIN, OUTPUT );
  pinMode( ESP8266_BLUE_PIN, OUTPUT );

  // Set ota details.
  ArduinoOTA.onStart(otaStarted);
  ArduinoOTA.onEnd(otaFinished);
  ArduinoOTA.onProgress(otaProgress);
  ArduinoOTA.onError(otaError);
  ArduinoOTA.setHostname("cabinets");
  ArduinoOTA.begin();

  // Create AP, if necessary
  wifi.autoConnect( ECHO_KEY_WORD ); 
  // wemoManager used for alexa interface.
  wemoManager.begin();

  // Set WIFI module to STA mode
  WiFi.mode( WIFI_STA );

  // Format: Alexa invocation name, local port no, on callback, off callback
  device = new WemoSwitch( ECHO_KEY_WORD, 80, LEDsOn, LEDsOff );
  wemoManager.addDevice( *device );

  // Report success.
  digitalWrite(ALARM_PIN, LOW);

  // Initialize Blynk.
  Blynk.config( auth );
}

void loop() 
{
  wemoManager.serverLoop();
  ArduinoOTA.handle();
  Blynk.run();
}

void syncPins()
{
  LEDsOn();

  if (red || green || blue || brightness)
  { 
    Blynk.virtualWrite(V29, 1);
    button = 1;
  }
  else 
  {
    Blynk.virtualWrite(V29, 0);
    button = 0;
  }
}

// Read value from r, g, b and brightness sliders.
BLYNK_WRITE(V30) { red = param.asInt();  syncPins(); }
BLYNK_WRITE(V31) { green = param.asInt(); syncPins(); }
BLYNK_WRITE(V32) { blue = param.asInt(); syncPins(); }
BLYNK_WRITE(V33) { brightness = param.asInt(); syncPins(); }

// Button.
BLYNK_WRITE(V29)
{
    button = param.asInt();
    
    if (button == 0) 
      LEDsOff();
    else if (red || green || blue || brightness)
      LEDsOn();
    else 
    {
      Blynk.virtualWrite(V29, 0);
      button = 0;
    }  
}

BLYNK_CONNECTED()
{
  if (isFirstConnect)
  {
    Blynk.syncAll();
    isFirstConnect = false;
  }
}
Posted in arduino, iot | Tagged , , , | Leave a comment

Controlling an Itead Sonoff B1 Color LED Bulb Using Blynk

The Itead Sonoff B1 color LED bulb is a 6W (2A maximum), 600lm output, RGB full color, dimmable bulb with an integrated ESP8285 WIFI chip. As purchased it can be controlled via the proprietary EWeLink application. However, I wanted to incorporate the bulb into my existing home automation project which is primarily controlled via a Blynk application on my android phone.

In order to fully control the bulb, the Blynk application will require at least 5 slider widgets (one each for the RGB, cool and white colors). An additional button widget can be added to provide instant on/off capability and a timer widget could be used for scheduled operation. Use of the timer widget option is not discussed here.

A quick search of the internet revealed directions on how to disassemble, connect to, and re-flash the ESP8285 chip. This process required soldering 4 wires to test points on the bulb’s PCB. The wires are then used to connect a USB-to-Serial type programmer. After reprogramming is completed, the wires are detached. For simplicity, I utilized the Arduino IDE development environment with the ESP8266 add-on for all of the programming needs.

To disassemble the bulb, I used a small plastic auto trim tool to assist in the removal of the translucent cover. My cover was lightly tacked in place through the use of 2 very small dabs of an extremely weak glue. I simply applied light pressure working my small plastic wedge around the perimeter of the cover, which easily separated. The cover simply snapped back in place.

My replacement firmware code is below. Please note my choice of Blynk virtual pin numbers is purely arbitrary. Unlike other advice found on the Internet, I used the Arduino IDE’s Generic ESP8285 board, 1M (64 SPIFFS) flash size and DOUT upload settings during programming. Good luck.

/*************************************************************************
 * Title: Simple ESP-8266 Wifi Sonoff B1 LED Light Controller
 * File: sonoff_echo_blynk_sonoff_b1.ino
 * Author: James Eli
 * Date: 2/7/2019
 *
 * This program controls a Sonoff B1 RGB LED light via the blynk app. 
 * 
 * Notes:
 *  (1) To place an ESP8266 into program mode, GPIO0 must be LOW during power up. 
 *  (2) See: https://github.com/arendst/Sonoff-Tasmota/wiki/Sonoff-B1-and-B1-R2
 *  and: https://tinkerman.cat/sonoff-b1-lights-and-shades/
 *        
 * Upload Settings:
 *   Board: Generic Generic ESP8285
 *   Flash size: 1M (64 SPIFFS)
 *   Flash Mode: DOUT
*************************************************************************
 * Change Log:
 *   1/14/2018: Initial release. JME
 *************************************************************************/
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>

// Blynk App authentication token, wifi ssid and password.
char auth[] = "..."; 
char ssid[] = "...";
char pass[] = "...";

// Sonoff B1/esp8266 pins.
#define DI_PIN  12 // GPIO12 
#define DCK_PIN 14 // GPIO14 
// Blynk virtual pins.
// V15 = red slider [0-255].
// V16 = green slider [0-255].
// V17 = blue slider [0-255].
// V19 = button [0-1]
// V20 = warm white slider [0-255].
// V21 = cool white slider [0-255].

// Slider values, initally set OFF.
int red = 0;
int green = 0;
int blue = 0;
int warmWhite = 0;
int coolWhite = 0;
// Button status.
int button = 0;

// Connect resync flag.
bool isFirstConnect = true;

// See MY9231 driver library @ https://github.com/xoseperez/my92xx
void pulseDI(uint8_t times)
{
  for (uint8_t i = 0; i < times; i++) 
  {
    digitalWrite(DI_PIN, HIGH);
    digitalWrite(DI_PIN, LOW);
  }
}

void pulseDCK(uint8_t times)
{
  for (uint8_t i = 0; i < times; i++) 
  {
    digitalWrite(DCK_PIN, HIGH);
    digitalWrite(DCK_PIN, LOW);
  }
}

void writeData(uint8_t data)
{
  // Send 8-bit data.
  for (uint8_t i = 0; i < 4; i++) 
  {
    digitalWrite(DCK_PIN, LOW);
    digitalWrite(DI_PIN, (data & 0x80));
    digitalWrite(DCK_PIN, HIGH);
    data = data << 1;
    digitalWrite(DI_PIN, (data & 0x80));
    digitalWrite(DCK_PIN, LOW);
    digitalWrite(DI_PIN, LOW);
    data = data << 1;
  }
}

void setupLED()
{
  // GPIO setup.
  pinMode(DI_PIN, OUTPUT);
  pinMode(DCK_PIN, OUTPUT);
  
  pulseDCK(64);                           // Clear all duty registers (2 chipes * 32).
  delayMicroseconds(12);                  // TStop > 12us.
  // Send 12 DI pulse, after 6 pulse's falling edge store duty data, and 12
  // pulse's rising edge convert to command mode.
  pulseDI(12);
  delayMicroseconds(12);                  // Delay >12us, begin send CMD data.
  // Send CMD data
  for (uint8_t n = 0; n < 2; n++)         // 2 chips in SONOFF B1.
    writeData(0x18);                      // ONE_SHOT_DISABLE, REACTION_FAST, BIT_WIDTH_8, FREQUENCY_DIVIDE_1, SCATTER_APDM
  delayMicroseconds(12);                  // TStart > 12us. Delay 12 us.
  // Send 16 DI pulse, at 14 pulse's falling edge store CMD data, and
  // at 16 pulse's falling edge convert to duty mode.
  pulseDI(16);
  delayMicroseconds(12);                  // TStop > 12us.
}

void setLED(uint8_t r, uint8_t g, uint8_t b, uint8_t w, uint8_t c)
{
  uint8_t duty[6] = { w, c, 0, g, r, b }; // RGBWC channels.

  delayMicroseconds(12);                  // TStop > 12us.
  for (uint8_t channel = 0; channel < 6; channel++) 
    writeData(duty[channel]);             // Send 8-bit Data.
  delayMicroseconds(12);                  // TStart > 12us. Ready for send DI pulse.
  pulseDI(8);                             // Send 8 DI pulse. After 8 pulse falling edge, store old data.
  delayMicroseconds(12);                  // TStop > 12us.
}

void setup() 
{
  // Init bulb LEDs.
  setupLED();
  // Set LEDs (initially off).
  setLED(red, green, blue, warmWhite, coolWhite);
  // Start Blynk.
  Blynk.begin(auth, ssid, pass);
}

void loop() { Blynk.run(); }

// Sync Blynk app and bulb. 
void syncPins()
{
  setLED(red, green, blue, warm, cool);

  if (button == 0 && (red || blue || green || warmWhite || coolWhite))
  { 
    Blynk.virtualWrite(V19, 1);
    button = 1;
  }
  
  if (button == 1 && !red && !blue && !green && !warmWhite && !coolWhite)
  {
    Blynk.virtualWrite(V19, 0);
    button = 0;
  }
}

// Read value from red, green, blue and brightness sliders.
BLYNK_WRITE(V15) { red = param.asInt(); syncPins(); }
BLYNK_WRITE(V16) { green = param.asInt(); syncPins(); }
BLYNK_WRITE(V17) { blue = param.asInt(); syncPins(); }
BLYNK_WRITE(V20) { warm = param.asInt(); syncPins(); }
BLYNK_WRITE(V21) { cool = param.asInt(); syncPins(); }

// Process Blynk ON/OFF button.
BLYNK_WRITE(V19)
{
  button = param.asInt();

  if (button == 1 && (red || blue || green || warmWhite || coolWhite))
    setLED(red, green, blue, warm, cool);
  else
  {
    setLED(0, 0, 0, 0, 0);
    Blynk.virtualWrite(V19, 0);
    button = 0;
  }
}

// Initial wifi connection sync.
BLYNK_CONNECTED()
{
  if (isFirstConnect)
  {
    Blynk.syncAll();
    isFirstConnect = false;
  }
}
Posted in iot | Tagged , , | Leave a comment

Using a Sonoff S31 with Blynk

testing

This post will describe how the Itead Sonofff S31 can control a household AC electric device with a cellphone loaded with the Blynk application. Additionally, we can use the S31 to monitor energy usage by keeping track of real-time power, current and voltage of the connected appliance.

s31

A Blynk timer widget allows automatic on/off control via a time schedule. The provided source code also includes Amazon Alexa voice command compatibility.

screenshot_2019-01-21-08-08-56

The Sonoff S31 incorporates an CSE7766 single-phase, multi-function measuring chip, which provides measurements of electric current, voltage and power through the ESP8266 UART. The CSE7766 operates at a baud rate of 4800bps (± 2%), with 8-bit data, 1 even parity check and 1 stop bit. The datasheet contains all the details and can be downloaded here.

powermeter

The S31 device was constructed in such a way that taking them apart is easy. You will need to solder 4 wires or a 4-pin header to the test points on the exposed PCB in order to upload the new firmware. Instead of duplicating the instructions for disassembly and reflashing of the Sonoff S31, simply see the blog here.

/*************************************************************************
 * Title: Simple ESP-8266 Amazon Echo/sonoff wifi relay control
 * File: sonoff_echo_blynk_Utility.ino
 * Author: James Eli
 * Date: 1/20/2019
 * 
 * Sonoff S31 ESP8266 "utility".
 *   Uses Blynk App V0 (switch), V1 (indicator LED), V2 (timer), V3 (voltage), 
 *   V4 (current), V5 (power), V6 (energy)
 *
 * This program controls a Sonoff wifi relay module communicating either 
 * through an amazon echo or the Blynk android application to the Sonoff 
 * onboard esp-8266 module. Amazon Echo responds to ECHO_KEY_WORD.
 *************************************************************************/
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <WiFiManager.h> 
#include <ESP8266WebServer.h>
#include <BlynkSimpleEsp8266.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <SimpleTimer.h>
#include "WemoSwitch.h"
#include "WemoManager.h"
#include "CallbackFunction.h"

bool isFirstConnect = true;       // Flag for re-sync on connection.
int relayState = LOW;             // Blynk app pushbutton status.

// Blynk app authentication code.
char auth[] = "...";

boolean SwitchReset = true;       // Flag indicating that the hardware button has been released

// esp8266 pins.
#define ESP8266_GPIO13  13        // Sonof green LED (LOW == ON).
#define ESP8266_GPIO0   0         // Sonoff pushbutton (LOW == pressed).
#define ESP8266_GPIO12  12        // Sonoff relay (HIGH == ON).
const int RELAY = ESP8266_GPIO12; // Relay switching pin. Relay is pin 12 on the SonOff
const int LED = ESP8266_GPIO13;   // On/off indicator LED. Onboard LED is 13 on Sonoff
const int SWITCH = ESP8266_GPIO0; // Pushbutton.

// CSE7766 data.
double power = 0;
double voltage = 0;
double current = 0;
double energy = 0;
double ratioV = 1.0;
double ratioC = 1.0;
double ratioP = 1.0;
// Serial data input buffer.
unsigned char serialBuffer[24];
// Serial error flags.
int error;
// Energy reset counter.
int energyResetCounter;
#define MAX_ENREGY_RESET_COUNT 12

// CSE7766 error codes.
#define SENSOR_ERROR_OK             0       // No error.
#define SENSOR_ERROR_OUT_OF_RANGE   1       // Result out of sensor range.
#define SENSOR_ERROR_WARM_UP        2       // Sensor is warming-up.
#define SENSOR_ERROR_TIMEOUT        3       // Response from sensor timed out.
#define SENSOR_ERROR_UNKNOWN_ID     4       // Sensor did not report a known ID.
#define SENSOR_ERROR_CRC            5       // Sensor data corrupted.
#define SENSOR_ERROR_I2C            6       // Wrong or locked I2C address.
#define SENSOR_ERROR_GPIO_USED      7       // The GPIO is already in use.
#define SENSOR_ERROR_CALIBRATION    8       // Calibration error or not calibrated.
#define SENSOR_ERROR_OTHER          99      // Any other error.
#define CSE7766_V1R                 1.0     // 1mR current resistor.
#define CSE7766_V2R                 1.0     // 1M voltage resistor.

// Amazon echo response term & Wifi access point name.
#define ECHO_KEY_WORD "utility"

WemoManager wemoManager;
WemoSwitch *device = NULL;

SimpleTimer timer;

void setup() 
{
  // WiFiManager intialization.
  WiFiManager wifi;               

  // Initialize pins.
  pinMode( RELAY, OUTPUT );
  pinMode( LED, OUTPUT );
  pinMode( SWITCH, INPUT_PULLUP );
  delay( 10 );
  // Switch relay off, LED on.
  digitalWrite( RELAY, LOW );
  digitalWrite( LED, LOW ); 

  // Create AP, if necessary
  wifi.autoConnect( ECHO_KEY_WORD ); 
  // wemoManager used for alexa interface.
  wemoManager.begin();

  // Set WIFI module to STA mode
  WiFi.mode( WIFI_STA );
  
  // Format: Alexa invocation name, local port no, on callback, off callback
  device = new WemoSwitch( ECHO_KEY_WORD, 80, RelayOn, RelayOff );
  wemoManager.addDevice( *device );

  // Initialize Blynk.
  Blynk.config( auth );

  // Set ota details.
  ArduinoOTA.setHostname( "utility" );
  ArduinoOTA.begin();

  // Setup cse7766 serial.
  Serial.flush();
  Serial.begin( 4800 );

  // Start a timer for checking button presses @ 100ms intervals.
  timer.setInterval( 100, ButtonCheck );

  // Start a timer for checking cse7766 power monitor @ 1000ms intervals.
  timer.setInterval( 1000, ReadCse7766 );
  
  // Switch LED off to signal initialization complete.
  digitalWrite( LED, HIGH );
}

// Main program loop.
void loop() 
{
  wemoManager.serverLoop();
  ArduinoOTA.handle();
  Blynk.run();
  timer.run();
}

// Toggle the relay on
void RelayOn() 
{
  digitalWrite( RELAY, HIGH );
  relayState = true;
  Blynk.virtualWrite( V0, HIGH ); // Sync the Blynk button widget state
  Blynk.virtualWrite( V1, relayState*255 );
}

// Toggle the relay off
void RelayOff() 
{
  digitalWrite( RELAY, LOW );
  relayState = false;
  Blynk.virtualWrite( V0, LOW ); // Sync the Blynk button widget state
  Blynk.virtualWrite( V1, relayState*255 );
}

// Handle switch changes originating on the Blynk app
BLYNK_WRITE( V0 ) 
{
  int SwitchStatus = param.asInt();
  
  if ( SwitchStatus )
    RelayOn();
  else 
    RelayOff();
}

// Handle timer changes originating from the Blynk app.
BLYNK_WRITE( V2 ) 
{
  int SwitchStatus = param.asInt();
  
  if ( SwitchStatus )
    RelayOn();
  else 
    RelayOff();
}

// This function runs every time Blynk connection is established.
BLYNK_CONNECTED() 
{
  if ( isFirstConnect ) 
  {
    Blynk.syncAll();
    isFirstConnect = false;
  }
}

// Handle hardware switch activation.
void ButtonCheck() 
{
  // look for new button press
  boolean SwitchState = ( digitalRead( SWITCH ) );
  
  // toggle the switch if there's a new button press
  if ( !SwitchState && SwitchReset == true ) 
  {
    if ( relayState )
      RelayOff();
    else
      RelayOn();
  
    // Flag that indicates the physical button hasn't been released
    SwitchReset = false;
    delay( 50 );            // De-bounce interlude.
  } 
  else if ( SwitchState ) 
  {
    // reset flag the physical button release
    SwitchReset = true;
  }
}

// Relay toggle helper function.
void ToggleRelay() 
{
  relayState = !relayState;
  
  if ( relayState ) 
    RelayOn();
  else 
    RelayOff();
}

// CSE7766 checksum.
bool CheckSum() 
{
  unsigned char checksum = 0;
  
  for (unsigned char i = 2; i < 23; i++) 
    checksum += serialBuffer[i];

  return checksum == serialBuffer[23];
}

// Process a cse7766 data packet.
void ProcessCse7766Packet() 
{
  // Confirm packet checksum.
  if ( !CheckSum() ) 
  {
    error = SENSOR_ERROR_CRC;
    return;
  }

  // Check for calibration error.
  if ( serialBuffer[0] == 0xAA ) 
  {
    error = SENSOR_ERROR_CALIBRATION;
    return;
  }
  if ( (serialBuffer[0] & 0xFC) == 0xFC ) 
  {
    error = SENSOR_ERROR_OTHER;
    return;
  }

  // Retrieve calibration coefficients.
  unsigned long coefV = (serialBuffer[2] << 16 | serialBuffer[3] << 8 | serialBuffer[4] );
  unsigned long coefC = (serialBuffer[8] << 16 | serialBuffer[9] << 8 | serialBuffer[10]);
  unsigned long coefP = (serialBuffer[14] << 16 | serialBuffer[15] << 8 | serialBuffer[16]);
  uint8_t adj = serialBuffer[20];

  // Calculate voltage.
  voltage = 0;
  if ( (adj & 0x40) == 0x40 ) 
  {
    unsigned long voltageCycle = serialBuffer[5] << 16 | serialBuffer[6] << 8 | serialBuffer[7];
    voltage = ratioV*coefV/voltageCycle/CSE7766_V2R;
  }

  // Calculate power.
  power = 0;
  if ( (adj & 0x10) == 0x10 ) 
  {
    if ( (serialBuffer[0] & 0xF2) != 0xF2 ) 
    {
      unsigned long powerCycle = serialBuffer[17] << 16 | serialBuffer[18] << 8 | serialBuffer[19];
      power = ratioP*coefP/powerCycle/CSE7766_V1R/CSE7766_V2R;
    }
  }

  // Calculate current.
  current = 0;
  if ( (adj & 0x20) == 0x20 ) 
  {
    if ( power > 0 ) 
    {
      unsigned long currentCycle = serialBuffer[11] << 16 | serialBuffer[12] << 8 | serialBuffer[13];
      current = ratioC*coefC/currentCycle/CSE7766_V1R;
    }
  }

  // Calculate energy.
  unsigned int difference;
  static unsigned int cfPulsesLast = 0;
  unsigned int cfPulses = serialBuffer[21] << 8 | serialBuffer[22];
  
  if (0 == cfPulsesLast) 
    cfPulsesLast = cfPulses;
  
  if (cfPulses < cfPulsesLast) 
    difference = cfPulses + (0xFFFF - cfPulsesLast) + 1;
  else
    difference = cfPulses - cfPulsesLast;
  
  energy += difference*(float)coefP/1000000.0;
  cfPulsesLast = cfPulses;

  // Energy reset.
  if ( power == 0 )
    energyResetCounter++;
  else
    energyResetCounter = 0;
  if ( energyResetCounter >= MAX_ENREGY_RESET_COUNT )
  {
    energy = 0.0;
    energyResetCounter = 0;
  }
   
  // Push data to Blynk app.
  Blynk.virtualWrite( V3, voltage ); // Voltage (Volts).
  Blynk.virtualWrite( V4, current ); // Current (Amps).
  Blynk.virtualWrite( V5, power );   // Power (Watts).
  Blynk.virtualWrite( V6, energy );  // Energy (kWh).
}

// Read serial cse7766 power monitor data packet.
void ReadCse7766() 
{
  // Assume a non-specific error.
  error = SENSOR_ERROR_OTHER; 
  static unsigned char index = 0;

  while ( Serial.available() > 0 )
  {
    uint8_t input = Serial.read();

    // first byte must be 0x55 or 0xF?.
    if ( index == 0 ) 
    { 
      if ( (input != 0x55) && (input < 0xF0) ) 
        continue;
    }
    // second byte must be 0x5A.
    else if ( index == 1 ) 
    {
      if ( input != 0x5A ) 
      {
        index = 0;
        continue;
      }
    }
    
    serialBuffer[index++] = input;
    
    if ( index > 23 ) 
    {
      Serial.flush();
      break;
    }
  }

  // Process packet.
  if ( index == 24 ) 
  {
    error = SENSOR_ERROR_OK;
    ProcessCse7766Packet();
    index = 0;
  }

  // Report error state (LED) of cse7766.
  if ( error == SENSOR_ERROR_OK )
    Blynk.virtualWrite( V18, 0 );
  else
    Blynk.virtualWrite( V18, 255 );
}
Posted in arduino, iot | Tagged , , | 4 Comments

Blynk & Amazon Echo (Alexa) Controlled Lights via Smart Wall Switch

Works like a charm. See this post for a discussion about reverse-engineering the wifi smart wall switch and how to program it. The Blynk application uploaded to the Lyasi smart switch is below.

/*************************************************************************
 * Title: Simple ESP-8266 Amazon Echo/sonoff wifi relay control
 * File: sonoff_echo.ino
 * Author: James Eli
 * Date: 5/1/2017
 *
 * For ESP8266 #5, "kitchen".
 * Uses Blynk virtual buttons:
 *   V0 button as switch
 *   V1 an indicator LED
 *   V2 on/off timer.
 *
 * This program controls an esp-8285 wifi switch/relay module communicating
 * either through an amazon echo or the Blynk android application to the
 * onboard esp-8285 module. Amazon Echo responds to ECHO_KEY_WORD.
 *************************************************************************
 * Change Log:
 *   1/26/2017: Initial release. JME
 *   5/12/2017: Modified for Vail house. JME
 *   6/11/2017: Added support for Blynk timer widget. JME
 *************************************************************************/
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <WiFiManager.h>
#include <ESP8266WebServer.h>
#include <BlynkSimpleEsp8266.h>
#include <WiFiUdp.h>
//#include <ArduinoOTA.h>
#include <SimpleTimer.h>
#include "WemoSwitch.h"
#include "WemoManager.h"
#include "CallbackFunction.h"

bool isFirstConnect = true;       // Flag for re-sync on connection.
int relayState = LOW;             // Blynk app pushbutton status.

// Blynk app authentication code goes here.
char auth[] = "...";

boolean SwitchReset = true;       // Flag indicating that the hardware button
                                  // has been released
// Esp8285 pins.
#define ESP8266_GPIO13 13         // blue LED (LOW == ON).
#define ESP8266_GPIO0   0         // proximity switching.
#define ESP8266_GPIO12 12         // relay (HIGH == ON).
const int RELAY = ESP8266_GPIO12; // Relay switching pin.
const int LED = ESP8266_GPIO13;   // On/Off indicator LED.
const int SWITCH = ESP8266_GPIO0; // Proximity switch.

// Amazon echo response term & Wifi access point name.
#define ECHO_KEY_WORD "kitchen"

WemoManager wemoManager;
WemoSwitch *device = NULL;
SimpleTimer timer;

void setup() {
  WiFiManager wifi;               // WiFiManager intialization.

  pinMode( RELAY, OUTPUT );       // Init pin modes.
  pinMode( LED, OUTPUT );
  pinMode( SWITCH, INPUT_PULLUP );

  delay( 10 );

  digitalWrite( RELAY, LOW );        // Turn relay off.
  digitalWrite( LED, LOW );          // Turn status LED on.
  wifi.autoConnect( ECHO_KEY_WORD ); // Create AP, if necessary
  wemoManager.begin();

  WiFi.mode( WIFI_STA );          // Set WIFI module to STA mode

  // Format: Alexa invocation name, local port no, on callback, off callback
  device = new WemoSwitch( ECHO_KEY_WORD, 80, RelayOn, RelayOff );
  wemoManager.addDevice( *device );
  Blynk.config( auth );
  //ArduinoOTA.begin();
  timer.setInterval( 100, ButtonCheck );
  digitalWrite( LED, HIGH );
}

void loop() {
  wemoManager.serverLoop();
  Blynk.run();
  //ArduinoOTA.handle();
  timer.run();
}

// Toggle the relay on
void RelayOn() {
  digitalWrite( RELAY, HIGH );
  relayState = true;
  Blynk.virtualWrite( V0, HIGH ); // Sync the Blynk button widget state
  Blynk.virtualWrite( V1, relayState*255 );
}

// Toggle the relay off
void RelayOff() {
  digitalWrite( RELAY, LOW );
  relayState = false;
  Blynk.virtualWrite( V0, LOW ); // Sync the Blynk button widget state
  Blynk.virtualWrite( V1, relayState*255 );
}

// Handle switch changes originating on the Blynk app
BLYNK_WRITE( V0 ) {
  int SwitchStatus = param.asInt();

  if ( SwitchStatus )
    RelayOn();
  else
    RelayOff();
}

// Handle timer changes originating from the Blynk app.
BLYNK_WRITE( V2 ) {
  int SwitchStatus = param.asInt();

  if ( SwitchStatus )
    RelayOn();
  else
    RelayOff();
}

// This function runs every time Blynk connection is established.
BLYNK_CONNECTED() {
  if ( isFirstConnect ) {
    Blynk.syncAll();
    isFirstConnect = false;
  }
}

// Handle hardware switch activation
void ButtonCheck() {
  // look for new button press
  boolean SwitchState = ( digitalRead( SWITCH ) );

  // toggle the switch if there's a new button press
  if ( !SwitchState && SwitchReset == true ) {
    if ( relayState )
      RelayOff();
    else
      RelayOn();

    // Flag that indicates the physical button hasn't been released
    SwitchReset = false;
    delay( 50 );            //debounce
  } else if ( SwitchState ) {
    // reset flag the physical button release
    SwitchReset = true;
  }
}
Posted in arduino, iot | Tagged , , | 2 Comments

Using Blynk to Control an RGB LED Strip with an ESP-8266

I am installing recessed RGB LED strip lighting under our kitchen cabinets and wanted to control them via Wifi and Amazon Alexa. I found and purchased an inexpensive ESP-8266 based Wifi controller on Amazon (see photo below). After a quick disassembly, I was easily able to reverse-engineer the device, and reprogram it for my purpose. This device came with an IR detector for use with an IR remote control, however, I didn’t use this and therefore removed the detector. I’ve subsequently noticed versions of the device are available on Amazon without the IR remote option.

Wifi RGB LED Strip Controller

The pinouts on the device were labelled R, G, B, with an additional common cathode pin and an un-populated pin labelled W (a fourth MOSFET is installed for this pin). The R-G-B pins are controlled by MOSFETs and connected to the following ESP-8266 GPIOs:

  • G – Q1 – Pin #14 GPIO05
  • R – Q2 – Pin #05 GPIO14
  • B – Q3 – Pin #06 GPIO12
  • IR – Pin #13 GPIO04
  • I didn’t trace the un-populated pin and MOSFET (labelled Q4).

    As suspected, the test points on the reverse side of the PCB are for programming the onboard ESP-8266 chip. Two of the test points were labelled RX and TX. I traced the other two points to GROUND and GPIO0, which are used to place the ESP-8266 into flash mode. Perfect! All I needed to do, was solder a few jumper wires to enable programming via a USB-Serial type device. Once the new firmware was uploaded I simply de-soldered the wires.

    For testing, I reprogrammed the device to respond to a simple Blynk IoT program with 3 slider widgets, one each for the R, G, B pins:

    /*************************************************************************
     * Title: Simple ESP-8266 Wifi RGB LED Strip Light Controller
     * File: esp8266_rgb_led_controller.ino
     * Author: James Eli
     * Date: 1/14/2018
     *
     * Blynk slider #1 (r) GPIO14
     * Blynk slider #2 (g) GPIO05
     * Blynk slider #3 (b) GPIO12
     * 
     * This program controls an RGB LED strip light. 
     * 
     * Notes:
     *  (1) Requires the following arduino libraries:
     *      ESP8266
     *  (2) Compiled with arduino ide 1.8.3
     *  (4) To place an ESP8266 into program mode, GPIO0 must be LOW during power up. 
     *  (5) Test Pads on PCB back:
     *        #1 – GND
     *        #2 – Pin #12 IO00
     *        TX – Pin #16 TX
     *        RX – Pin #15 RX
    *************************************************************************/
    #include <ESP8266WiFi.h>
    #include <BlynkSimpleEsp8266.h>
    
    // Blynk App authentication token, wifi ssid and password.
    char auth[] = "***";
    char ssid[] = "***";
    char pass[] = "***";
    
    // Esp8266 pins.
    const int RED_PIN   = 14;
    const int GREEN_PIN = 5;
    const int BLUE_PIN  = 12;
    
    void setup() {
      pinMode( RED_PIN, OUTPUT );
      pinMode( GREEN_PIN, OUTPUT );
      pinMode( BLUE_PIN, OUTPUT );
      Blynk.begin( auth, ssid, pass );
    }
    
    void loop() {
      Blynk.run();
    }
    
    Posted in iot | Tagged , , | Leave a comment