Pine

V1.0

🌲 Indoor Infrared receiver and transmitter with temperature sensor 🎐

power AA
wireless Infrared
sensor Temperature
mcu SAMD21G18
bom items 49
bom cost USD $9.71
vendors 2
status completed in September 2020

Features

  • Record and store the ON and OFF aircon commands with IR receiver
  • Turn on or off the aircon with the IR emitter
  • Measure temperature of another part of the room
  • Go into sleep mode after turning on or off the aircon

Firmware

Download code
// #define DEBUG

#include "DebugUtils.h"
#include "src/serial/serial.h"
#include "src/sensor/sensor.h"
#include "src/webusb/webusb.h"
#include "src/infrared/infrared.h"
#include "src/led/led.h"
#include <FlashStorage.h>

#define MAX_IR_LEN 300
typedef struct {
  int sizeofON;
  uint16_t rawDataON[MAX_IR_LEN];
  int sizeofOFF;
  uint16_t rawDataOFF[MAX_IR_LEN];
  int interval;
  int duration;
  int temperature;
  bool valid;
} IRRawCode;
IRRawCode userConfig;
IRRawCode readConfig;

int config[3];
int configIndex = 0;
bool isRecordingON = false;
bool isRecordingOFF = false;
bool isConfigMode = false;
bool hasSentUserConfig = false;

FlashStorage(my_flash_store, IRRawCode);

void setup() {
  #ifdef DEBUG
  initSerial();
  SerialUSB.println("Start Pine...");
  #endif

  initWebUSBSerial();

  if (initSensor()) {
    DEBUG_PRINT(readTemperature());
    DEBUG_PRINT(readHumidity());
  } else {
    DEBUG_TITLE("Error: Si7021 Sensor not found.")
  }

  initIR();
  initLED();
  readConfig = my_flash_store.read();
}

void loop() {
  // Scenario: Very first setup
  if (!readConfig.valid && !isConfigMode) {
    DEBUG_TITLE("Add user config at https://hutscape.com/pine/webusb");
    blink();
  }

  // Scenario: Normal operation
  if (readConfig.valid && !isConfigMode) {
    DEBUG_TITLE("Do demo task: Turn on/off aircon every 5s");
    doTask();
  }

  // Scenario: Config mode
  if (isConfigMode) {
    if (!hasSentUserConfig && readConfig.valid) {
      DEBUG_TITLE("Sending user config to web browser");
      sendUserConfig();
      hasSentUserConfig = true;
    }

    if (receiveIR()) {
      if (isValidIRCode()) {
        receiveIRFromUser();
      }
      enableIR();
    }
  }

  // All scenarios: alway check if web usb is connected
  if (isWebUSBAvailable()) {
    receiveConfigFromUser(readWebUSB());
  }
}

// Print out raw IR code received from the user pressing a remote control
void receiveIRFromUser() {
  int irSize = getIRcodeSize();
  uint16_t irCode[MAX_IR_LEN];
  getIRCode(irCode, MAX_IR_LEN);

  DEBUG_TITLE("Received user IR code...");

  #ifdef DEBUG
  SerialUSB.print(F("#define RAW_DATA_LEN "));
  SerialUSB.println(irSize, DEC);
  SerialUSB.print(F("uint16_t rawData[RAW_DATA_LEN]={\n"));

  for (int i = 1; i < irSize; i++) {
    SerialUSB.print(irCode[i], DEC);
    SerialUSB.print(F(", "));
  }

  SerialUSB.println(F("1000};\n"));
  #endif

  // TODO: Abstract out into a single function
  if (isRecordingON) {
    userConfig.sizeofON = irSize;

    for (int i = 0; i < irSize - 1; i++) {
      userConfig.rawDataON[i] = irCode[i+1];
    }

    userConfig.rawDataON[irSize - 1] = 1000;
    isRecordingON = false;
  } else if (isRecordingOFF) {
    userConfig.sizeofOFF = irSize;

    for (int i = 0; i < irSize - 1; i++) {
      userConfig.rawDataOFF[i] = irCode[i+1];
    }

    userConfig.rawDataOFF[irSize - 1] = 1000;
    isRecordingOFF = false;
  }

  writeWebUSB((const uint8_t *)irCode, irSize*2);
}

// Emit IR code that is stored in the flash memory
void doTask() {
  delay(5000);
  sendIR(readConfig.rawDataON, readConfig.sizeofON, 36);
  DEBUG_TITLE("Sent Turn ON Aircon");

  delay(5000);
  sendIR(readConfig.rawDataOFF, readConfig.sizeofOFF, 36);
  DEBUG_TITLE("Sent Turn OFF Aircon");
}

// Read config values from the user and store in flash memory
void receiveConfigFromUser(int byte) {
  if (byte == 'A') {
    DEBUG_TITLE("Recording ON IR command");
    isRecordingON = true;
  } else if (byte == 'B') {
    DEBUG_TITLE("Recording OFF IR command");
    isRecordingOFF = true;
  } else if (byte == '1') {
    DEBUG_TITLE("Connected via Web USB");
    isConfigMode = true;
    hasSentUserConfig = false;
  } else {
    config[configIndex++] = byte;
    if (configIndex == 3) {
      configIndex = 0;
      DEBUG_TITLE("Received user config from the browser");

      userConfig.interval = config[0];
      DEBUG_PRINT(userConfig.interval);

      userConfig.duration = config[1];
      DEBUG_PRINT(userConfig.duration);

      userConfig.temperature = config[2];
      DEBUG_PRINT(userConfig.temperature);

      printWebUSB("From MCU: Received all user configurations.");
      userConfig.valid = true;
      isConfigMode = false;

      storeUserConfig();
    }
  }
}

void sendUserConfig() {
  DEBUG_PRINT(readConfig.interval);
  DEBUG_PRINT(readConfig.duration);
  DEBUG_PRINT(readConfig.temperature);

  // NOTE: Send to the web browser as a JSON string
  String config = "{\"interval\":"
    + String(readConfig.interval)
    + ", \"duration\":"
    + String(readConfig.duration)
    + ", \"temperature\":"
    + String(readConfig.temperature)
    + "}";

  printWebUSB(config);
}

void storeUserConfig() {
  my_flash_store.write(userConfig);
  readConfig = my_flash_store.read();
}

Makefile

BOARD?=arduino:samd:arduino_zero_native
PORT := $(shell ls /dev/cu.usbmodem*)

.PHONY: default lint all flash clean

default: lint all flash clean

lint:
	cpplint --extensions=ino --filter=-legal/copyright,-whitespace/line_length,-readability/casting,-readability/todo *.ino

all:
	# This custom PCB does not have a crytal on pins PA00 and PA01
	# Hence, use -DCRYSTALLESS to replace the extra_flags in boards.txt
	arduino-cli compile --fqbn $(BOARD) --build-properties build.extra_flags="-DCRYSTALLESS -D__SAMD21G18A__ {build.usb_flags}" ./
flash:
	arduino-cli upload -p $(PORT) --fqbn $(BOARD)

server:
	echo "Open Chrome browser at http://localhost:8000"
	python -m SimpleHTTPServer 8000

clean:
	rm -r build

Serial console

Firmware serial console

Test

Download test code
#include "Adafruit_Si7021.h"
#include <IRLibSendBase.h>
#include <IRLib_HashRaw.h>
#include <IRLibRecvPCI.h>
#include <WebUSB.h>
#include "./data.h"

#define LED 13
// https://github.com/cyborg5/IRLib2/blob/master/IRLibProtocols/IRLibSAMD21.h#L38-L39
// For SAMD21 boards: We are recommending using Pin 5 for receiving
#define IR_RECEIVE_PIN 5

// https://github.com/cyborg5/IRLib2/blob/master/IRLibProtocols/IRLibSAMD21.h#L40-L41
// For SAMD21 boards: For sending, default pin is 9
#define IR_EMIT_PIN 9

Adafruit_Si7021 sensor = Adafruit_Si7021();
WebUSB WebUSBSerial(1, "webusb.github.io/arduino/demos/console");
IRrecvPCI myReceiver(IR_RECEIVE_PIN);
IRsendRaw mySender;
String readString;
int test = 0;

void setup() {
  SerialUSB.begin(9600);
  while (!SerialUSB) {}

  WebUSBSerial.begin(9600);

  delay(1000);

  pinMode(LED, OUTPUT);
  myReceiver.enableIRIn();

  SerialUSB.println("Starting Pine design verification test!");
  SerialUSB.println("-------------------------------------");

  SerialUSB.println("\n\nTest 1: It expects to turn ON and OFF the LED");
  blink(5);
  delay(1000);

  SerialUSB.println("\n\nTest 2: It expects to measure the humidity and temp");
  if (initSensor()) {
    SerialUSB.print("Humidity: ");
    SerialUSB.print(sensor.readHumidity(), 2);
    SerialUSB.println(" RH%");

    SerialUSB.print("Temperature: ");
    SerialUSB.print(sensor.readTemperature(), 2);
    SerialUSB.println(" C");
  }
  delay(1000);

  SerialUSB.println("\n\nTest 3: It expects to turn ON the aircon");
  SerialUSB.print("Have you pointed the circuit to the aircon? [y/n]: ");
  test = 0;
  while (test == 0) {
    while (SerialUSB.available()) {
      delay(3);
      char c = SerialUSB.read();
      readString += c;
    }

    if (readString == "y") {
      SerialUSB.println(readString);
      mySender.send(rawDataON, RAW_DATA_LEN, 38);
      SerialUSB.println("Sent Turn ON Aircon");

      readString = "";
      test = 1;
    }
  }
  delay(1000);

  SerialUSB.println("\n\nTest 4: It expects to turn OFF the aircon");
  SerialUSB.print("Have you pointed the circuit to the aircon? [y/n]: ");
  test = 0;
  while (test == 0) {
    while (SerialUSB.available()) {
      delay(3);
      char c = SerialUSB.read();
      readString += c;
    }

    if (readString == "y") {
      SerialUSB.println(readString);
      mySender.send(rawDataOFF, RAW_DATA_LEN, 38);
      SerialUSB.println("Sent Turn OFF Aircon");

      readString = "";
      test = 1;
    }
  }
  delay(1000);

  SerialUSB.println("\n\nTest 5: It expects to receive IR signals");
  SerialUSB.print("Have you pressed and pointed the remote control to Pine? [y/n]: ");
  test = 0;
  while (test == 0) {
    while (SerialUSB.available()) {
      delay(3);
      char c = SerialUSB.read();
      readString += c;
    }

    if (myReceiver.getResults()) {
      SerialUSB.print(" Received IR signal length: ");
      SerialUSB.print(recvGlobal.recvLength, DEC);
      SerialUSB.print("     ");
      myReceiver.enableIRIn();
    }

    if (readString == "y") {
      SerialUSB.println(readString);
      readString = "";
      test = 1;
    }
  }

  SerialUSB.println("\n\nTest 6: It expects to receive info from the browser");
  SerialUSB.print("Have you turned on/off led from hutscape.com/webusb-led? [y/n]: ");
  test = 0;
  while (test == 0) {
    while (SerialUSB.available()) {
      delay(3);
      char c = SerialUSB.read();
      readString += c;
    }

    if (readString == "y") {
      SerialUSB.println(readString);
      readString = "";
      test = 1;
    }

    if (WebUSBSerial && WebUSBSerial.available()) {
      int byte = WebUSBSerial.read();

      if (byte == 'H') {
        SerialUSB.print("LED ON     ");
        WebUSBSerial.write("Turning LED on.");
        digitalWrite(LED, HIGH);
      } else if (byte == 'L') {
        SerialUSB.print("LED OFF     ");
        WebUSBSerial.write("Turning LED off.");
        digitalWrite(LED, LOW);
      }

      WebUSBSerial.flush();
    }
  }
  delay(1000);

  SerialUSB.println("\n\nTest 7: It expects to send info to the browser");
  SerialUSB.print("Have you open the dev console at hutscape.com/webusb-send? [y/n]: ");
  test = 0;
  while (test == 0) {
    while (SerialUSB.available()) {
      delay(3);
      char c = SerialUSB.read();
      readString += c;
    }

    if (readString == "y") {
      SerialUSB.println(readString);
      readString = "";
      test = 1;

      WebUSBSerial.write("Hello from mcu to the browser!");
      WebUSBSerial.flush();
    }
  }
  delay(1000);

  SerialUSB.println("\n\n-------------------------------------");
  SerialUSB.println("Completed Pine design verification test!");
  SerialUSB.println("-------------------------------------");
}

void loop() { }

void blink(int num) {
  for (int i=0; i < num; i++) {
    SerialUSB.print(i+1);
    SerialUSB.print("  ");

    digitalWrite(LED, LOW);
    delay(500);
    digitalWrite(LED, HIGH);
    delay(500);
  }
}

bool initSensor() {
  if (!sensor.begin()) {
    SerialUSB.println("\nERROR: Did not find Si7021 sensor!");
    return false;
  }

  return true;
}

Makefile

BOARD?=arduino:samd:arduino_zero_native
PORT := $(shell ls /dev/cu.usbmodem*)

.PHONY: default lint all flash clean

default: lint all flash clean

lint:
	cpplint --extensions=ino --filter=-legal/copyright,-whitespace/line_length *.ino

all:
	# This custom PCB does not have a crytal on pins PA00 and PA01
	# Hence, use -DCRYSTALLESS to replace the extra_flags in boards.txt
	arduino-cli compile --fqbn $(BOARD) --build-properties build.extra_flags="-DCRYSTALLESS -D__SAMD21G18A__ {build.usb_flags}" ./

flash:
	arduino-cli upload -p $(PORT) --fqbn $(BOARD)

server:
	echo "Open Chrome browser at http://localhost:8000"
	python -m SimpleHTTPServer 8000

clean:
	rm -r build

Serial console

Firmware serial console