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:mzero_bl
PORT := $(shell ls /dev/cu.usbmodem*)

.PHONY: lint compile upload clean

default: lint all flash clean

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

all:
	arduino-cli compile --fqbn $(BOARD) ./

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

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

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>