V1.0
🌲 Write Project discreption here 🎐
// #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 buildSerial console
  #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 buildSerial console
  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 Pine@Hutscape</h2>
    </div>
  </div>
</section>
<section class="section is-small">
  <div class="container">
    <div class="content is-large">
      <p id="info"></p>
    </div>
  </div>
</section>
<section class="section is-small is-hidden" id="configure">
  <div class="container">
    <!-- TODO: Aircon ON / OFF commands should be able to be read again and tested before submitting -->
    <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>
    <form id="form">
      <div class="field">
        <label class="label is-large">Interval (minutes)</label>
        <div class="control">
          <input class="input is-large" id="interval" required="required" 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" required="required" 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" required="required" 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>
    </form>
  </div>
</section>
<section class="section is-small">
  <div class="container">
    <div class="control">
      <input class="button is-info is-large" id="connect" type="submit" value="Connect to Pine">
    </div>
  </div>
</section>
<script src="serial.js"></script>
<script>
var port
let formSection = document.querySelector('#form')
let connectButton = document.querySelector('#connect')
let submitButton = document.querySelector('#submit')
let intervalEl = document.getElementById("interval")
let durationEl = document.getElementById("duration")
let temperatureEl = document.getElementById("temperature")
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()
var irCode = []
connectButton.addEventListener('click', function() {
  if (port) { // If port is already connected, disconnect it
    if (!formSection.checkValidity()) {
      console.log("Form fields are not valid. Please check.")
      return
    }
    let config = new Uint8Array(3)
    config[0] = document.getElementById("interval").value
    config[1] = document.getElementById("duration").value
    config[2] = document.getElementById("temperature").value
    console.log("Sending config to MCU")
    port.send(config).catch(error => {
      console.log('Send error: ' + error)
    })
    // FIXME: Disconnect only after successfully sending all the config to the mcu
    // Not after 1 second delay
    setTimeout(function() {
      port.disconnect()
      port = null
      connectButton.value = 'Connect to Pine'
      configureSection.classList.add("is-hidden")
      infoSection.innerHTML = 'User config stored successfully!'
    }, 1000);
  } else { // If there is no port, then connect to a new port
    console.log("Connect!")
    serial.requestPort().then(selectedPort => {
      port = selectedPort
      configureSection.classList.remove("is-hidden")
      port.connect().then(() => {
        infoSection.innerHTML = '<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)
        port.send(textEncoder.encode('1')).catch(error => {
          console.log('Send error: ' + error)
        })
        connectButton.value = 'Submit'
        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) })
  }
})
// TODO: Refactor recordONButton and recordOFFButton into a single function
recordONButton.addEventListener('click', function() {
  recordONButton.classList.add("is-warning")
  recordONButton.textContent = 'Recording ON Command...'
  isRecordingON = true
  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)
  })
})
function processReceievedData(data) {
  var str = textDecoder.decode(data)
  // Data received: string message "From MCU: "
  if (isMessage(str)) {
    console.log(str)
    return
  }
  // Data received type: JSON user config
  if (isJson(str)) {
    var userConfig = JSON.parse(str)
    console.log(userConfig)
    intervalEl.value = userConfig.interval
    durationEl.value = userConfig.duration
    temperatureEl.value = userConfig.temperature
    return
  }
  // Data received type: int 16 array for IR raw code
  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 = []
  }
}
function isJson(str) {
  try {
    JSON.parse(str);
  } catch (e) {
    return false;
  }
  return true;
}
function isMessage(str) {
  // A message from the micro-controller will start with "From MCU"
  return str.substring(0, 8) === "From MCU"
}
</script>
</body>