V1.0
🌲 Control aircon by monitoring temperature in other parts of the room 🎐
// #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
#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
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="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>