Pine

V1.0

🌲 Control aircon by monitoring temperature in other parts of the room 🎐

power AA
wireless Infrared
sensor Temperature
mcu SAMD21G
bom items 49
bom cost USD $9.71
vendors 2
status ongoing

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
#include <IRLibAll.h>
#include <IRLibSendBase.h>
#include <IRLib_HashRaw.h>
#include "Adafruit_Si7021.h"
#include <FlashStorage.h>
#include "./data.h"
#include <WebUSB.h>

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

IRrecvPCI myReceiver(IR_RECEIVE_PIN);
IRsendRaw mySender;

Adafruit_Si7021 sensor = Adafruit_Si7021();

typedef struct {
  int sizeofON;
  uint16_t rawDataON[400];
  int sizeofOFF;
  uint16_t rawDataOFF[400];
  int interval;
  int duration;
  int temperature;
} IRRawCode;
FlashStorage(my_flash_store, IRRawCode);
IRRawCode userConfig;
IRRawCode userConfigRead;

WebUSB WebUSBSerial(1, "hutscape.com/pine/webusb/");
int config[3];
int configIndex;
bool isRecordingON = false;
bool isRecordingOFF = true;

void setup() {
  initSerial();
  initWebUSBSerial();

  if (initSensor()) {
    readSensor();
  }

  initIR();
}

void loop() {
  // IR Receive
  if (myReceiver.getResults()) {
    if (isValidIRCode()) {
      SerialUSB.println("\n\nReceived user IR code...");
      SerialUSB.print(F("#define RAW_DATA_LEN "));
      SerialUSB.println(recvGlobal.recvLength, DEC);
      SerialUSB.print(F("uint16_t rawData[RAW_DATA_LEN]={\n"));

      for (bufIndex_t i = 1; i < recvGlobal.recvLength; i++) {
        SerialUSB.print(recvGlobal.recvBuffer[i], DEC);
        SerialUSB.print(F(", "));
        if ((i % 8) == 0) {
          SerialUSB.print(F("\n"));
        }
      }

      if (isRecordingON) {
        for (bufIndex_t i = 1; i < recvGlobal.recvLength; i++) {
          userConfig.rawDataON[i] = recvGlobal.recvBuffer[i];
        }

        recvGlobal.recvBuffer[recvGlobal.recvLength] = 1000;
        userConfig.sizeofON = RAW_DATA_LEN;
        userConfig.sizeofOFF = RAW_DATA_LEN;

        isRecordingON = false;

        SerialUSB.println("***************************");
        SerialUSB.println("Recoded ON command:");

        SerialUSB.print(F("uint16_t rawData[RAW_DATA_LEN]={\n"));
        for (bufIndex_t i = 1; i < recvGlobal.recvLength; i++) {
          SerialUSB.print(userConfig.rawDataON[i], DEC);
          SerialUSB.print(F(", "));
          if ((i % 8) == 0) {
            SerialUSB.print(F("\n"));
          }
        }
        SerialUSB.println("***************************");
      }

      WebUSBSerial.write((const uint8_t *)recvGlobal.recvBuffer,
        recvGlobal.recvLength*2);
      SerialUSB.println(F("1000};"));
      WebUSBSerial.flush();
    }

    myReceiver.enableIRIn();
  }

  // IR Emit
  char input = SerialUSB.read();

  if (SerialUSB.read() == '1') {
    mySender.send(rawDataON, RAW_DATA_LEN, 36);
    SerialUSB.println("Sent Turn ON Aircon");
  } else if (SerialUSB.read() == '0') {
    mySender.send(rawDataOFF, RAW_DATA_LEN, 36);
    SerialUSB.println("Sent Turn OFF Aircon");
  }

  // Read user config from browser
  if (WebUSBSerial && WebUSBSerial.available()) {
    int byte = WebUSBSerial.read();

    if (byte == 'A') {
      SerialUSB.println("Recording ON IR command");
      isRecordingON = true;
    } else if (byte == 'B') {
      SerialUSB.println("Recording OFF IR command");
      isRecordingOFF = true;
    } else {
      config[configIndex++] = byte;
      if (configIndex == 3) {
        SerialUSB.println("\nReceived user config...");
        SerialUSB.print("Interval: ");
        SerialUSB.print(config[0]);
        SerialUSB.println(" minutes");
        userConfig.interval = config[0];

        SerialUSB.print("Duration: ");
        SerialUSB.print(config[1]);
        SerialUSB.println(" hours");
        userConfig.duration = config[1];

        SerialUSB.print("Ideal temperature: ");
        SerialUSB.print(config[2]);
        SerialUSB.println(" C");
        userConfig.temperature = config[2];

        WebUSBSerial.print("Received interval, duration and ideal temperature:");
        WebUSBSerial.flush();
        configIndex = 0;

        // storeIRCode();
      }
    }
  }
}

void initSerial() {
  SerialUSB.begin(9600);
  while (!SerialUSB) {}
  delay(100);

  SerialUSB.println("Start!");
}

void initWebUSBSerial() {
  WebUSBSerial.begin(9600);
  delay(100);

  WebUSBSerial.flush();
}

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

  return true;
}

void initIR() {
  myReceiver.enableIRIn();
  SerialUSB.println("\nReady to receive user config...");
}

void readSensor() {
  SerialUSB.println("\nReading sensor data... ");

  SerialUSB.print("Humidity: ");
  SerialUSB.print(sensor.readHumidity(), 2);
  SerialUSB.println(" RH%");

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

bool isValidIRCode() {
  // Total array length of the raw IR code should be more than 10
  // First array element of the raw IR code should be more than 3000
  if ((int) recvGlobal.recvLength > 10 && (int) recvGlobal.recvBuffer[1] > 3000) {
    return true;
  }

  return false;
}

void storeIRCode() {
  for (int i = 0; i < RAW_DATA_LEN; i++) {
    userConfig.rawDataON[i] = rawDataON[i];
    userConfig.rawDataOFF[i] = rawDataOFF[i];
  }

  userConfig.sizeofON = RAW_DATA_LEN;
  userConfig.sizeofOFF = RAW_DATA_LEN;

  my_flash_store.write(userConfig);
  SerialUSB.println("Writing in flash completed.");

  userConfigRead = my_flash_store.read();

  SerialUSB.print("\nLength of rawDataON: ");
  SerialUSB.println(userConfigRead.sizeofON);
  SerialUSB.print("\nLength of rawDataOFF: ");
  SerialUSB.println(userConfigRead.sizeofOFF);

  SerialUSB.println("\nrawDataON array: ");
  for (int i = 0; i < RAW_DATA_LEN; i++) {
    SerialUSB.print(userConfigRead.rawDataON[i]);
    SerialUSB.print(", ");
  }

  SerialUSB.println("\nrawDataOFF array: ");
  for (int i = 0; i < RAW_DATA_LEN; i++) {
    SerialUSB.print(userConfigRead.rawDataOFF[i]);
    SerialUSB.print(", ");
  }

  SerialUSB.println("\n\n");
}

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 *.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}" ./ --verbose
	arduino-cli compile --fqbn $(BOARD) ./ --verbose

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

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

clean:
	rm -f .*.bin
	rm -f .*.elf
	rm -f .*.hex

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

Web USB

Web USB on Chrome browser is used to take in user configuration about how they want to control their aircon.

Download code View demo
<!doctype html>
<html>
<head>
  <title>Configure Pine</title>
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
</head>
<body>
<section class="hero is-small is-light">
  <div class="hero-body">
    <div class="container">
      <h2 class="subtitle is-1">Configure [email protected]</h2>
    </div>
  </div>
</section>

<section class="section is-small">
  <div class="container">
    <div class="control">
      <button class="button is-info is-large" id="connect">Connect to Pine</button>
      <div class="content is-large">
        <p id="info"></p>
      </div>
    </div>
  </div>
</section>

<section class="section is-small is-hidden" id="configure">
  <div class="container">
    <div class="field">
      <label class="label is-large">Aircon ON</label>
      <button id="on" class="button is-info">Record ON command</button>
      <p class="help">Point the aircon remote controller to Pine and press the on button.</p>
      <p id="on-command"></p>
    </div>

    <div class="field">
      <label class="label is-large">Aircon OFF</label>
      <button id="off" class="button is-info">Record OFF command</button>
      <p class="help">Point the aircon remote controller to Pine and press the off button.</p>
      <p id="off-command"></p>
    </div>

    <div class="field">
      <label class="label is-large">Interval (minutes)</label>
      <div class="control">
        <input class="input is-large" id="interval" type="number" min=10 placeholder="Interval (mins)">
        <p class="help">Pine will check for the room temperature every interval set. Example every 20 minutes.</p>
      </div>
    </div>

    <div class="field">
      <label class="label is-large">Total duration (hours)</label>
      <div class="control">
        <input class="input is-large" id="duration" type="number" min=1 placeholder="Total period (hours)">
        <p class="help">Pine will check for the room temperature every interval set for a total of this duration.</p>
      </div>
    </div>

    <div class="field">
      <label class="label is-large">Ideal room temperature (C)</label>
      <div class="control">
        <input class="input is-large" id="temperature" type="number" min=15 max=28 placeholder="Ideal temperature (C)">
        <p class="help">Pine will turn on or off the aircon according to this temperature.</p>
      </div>
    </div>

    <div class="field">
      <button class="button is-primary is-large" id="submit">Submit</button>
    </div>
  </div>
</section>

<script src="serial.js"></script>
<script>
var port
let connectButton = document.querySelector('#connect')
let submitButton = document.querySelector('#submit')

let recordONButton = document.querySelector('#on')
var isRecordingON = false
let recordOFFButton = document.querySelector('#off')
var isRecordingOFF = false

let configureSection = document.querySelector('#configure')
let infoSection = document.querySelector('#info')
let textEncoder = new TextEncoder()
let textDecoder = new TextDecoder()
let intervalValue = document.getElementById("interval").value
var irCode = []

submitButton.addEventListener('click', function() {
  if (port !== undefined) {
    let config = new Uint8Array(3)
    config[0] = document.getElementById("interval").value
    config[1] = document.getElementById("duration").value
    config[2] = document.getElementById("temperature").value
    port.send(config);

    console.log('Submitting interval, duration and temperature.')
  }
})

recordONButton.addEventListener('click', function() {
  recordONButton.classList.add("is-warning")
  recordONButton.textContent = 'Recording ON Command...'
  isRecordingON = true
  console.log("Waiting for ON command...")
  port.send(textEncoder.encode('A')).catch(error => {
    console.log('Send error: ' + error)
  })
})

recordOFFButton.addEventListener('click', function() {
  recordOFFButton.classList.add("is-warning")
  recordOFFButton.textContent = 'Recording OFF Command...'
  isRecordingOFF = true
  port.send(textEncoder.encode('B')).catch(error => {
    console.log('Send error: ' + error)
  })
})

connectButton.addEventListener('click', function() {
  if (port) { // If port is already connected, disconnect it
    connectButton.textContent = 'Connect'
    configureSection.classList.add("is-hidden")
    infoSection.innerHTML = ''
    port.disconnect()
    port = null
    console.log('Pine is disconnected.')
  } else { // If there is no port, then connect to a new port
    serial.requestPort().then(selectedPort => {
      port = selectedPort
      configureSection.classList.remove("is-hidden")
      port.connect().then(() => {
        infoSection.innerHTML = '<br><strong>Pine is now connected via Web USB!</strong>' +
          '<br>Product ID: 0x' + port.device_.productId.toString(16) +
          '<br>Vendor ID: 0x' + port.device_.vendorId.toString(16)

        // console.log('Pine is now connected via Web USB!')
        // console.log('Pine Product ID: 0x' + port.device_.productId.toString(16))
        // console.log('Pine Vendor ID: 0x' + port.device_.vendorId.toString(16))

        connectButton.textContent = 'Disconnect'
        port.onReceive = data => { processReceievedData(data) }
        port.onReceiveError = error => { console.log('Receive error: ' + error)}
      }, error => { console.log('Connection error: ' + error) })
    }).catch(error => { console.log('Connection error: ' + error) })
  }
})

function processReceievedData(data) {
  // Other texts
  // console.log(textDecoder.decode(data))

  // IR Code array
  var irCodeReceieved = new Int16Array(data.buffer)

  if (irCodeReceieved.length == 32) {
    irCodeReceieved.forEach(function(el) {
      irCode.push(el)
    })
  } else if (irCodeReceieved.length < 32 && irCodeReceieved.length > 0) {
    irCodeReceieved.forEach(function(el) {
      irCode.push(el)
    })

    irCode.shift() // Remove the first element which is always garbage
    irCode.push(1000) // Add 1000 as the last element

    if (irCode.length == 292) { // Valid IR Code is 292
      if (isRecordingON) {
        document.getElementById('on-command').innerText = "Received ON command: [" + irCode.join(', ') + "]"
        recordONButton.classList.remove("is-warning")
        recordONButton.textContent = 'Record ON Command'
        isRecordingON = false
      }

      if (isRecordingOFF) {
        document.getElementById('off-command').innerText = "Received OFF command: [" + irCode.join(', ') + "]"
        recordOFFButton.classList.remove("is-warning")
        recordOFFButton.textContent = 'Record OFF Command'
        isRecordingOFF = false
      }
    }

    irCode = [] // Clear array
  }
}
</script>
</body>