Compare commits

..

4 Commits

6 changed files with 192 additions and 85 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
config.h config.h
*.bin *.bin
*.elf *.elf
configs/

View File

@ -6,7 +6,7 @@ UPLOAD_PORT ?= /dev/ttyUSB0
all: build all: build
build: build:
arduino-cli compile -b ${BOARD_NAME} smartswitch.ino arduino-cli compile -b ${BOARD_NAME} ${PWD}
upload: upload:
arduino-cli upload -b ${BOARD_NAME} -p ${UPLOAD_PORT} smartswitch.ino arduino-cli upload -b ${BOARD_NAME} -p ${UPLOAD_PORT} ${PWD}

View File

@ -1,3 +1,7 @@
// Do not edit these two lines
#ifndef __CONFIG_H__
#define __CONFIG_H__
#define WIFI_SSID "myNetwork" #define WIFI_SSID "myNetwork"
#define WIFI_PASSWORD "myPassword" #define WIFI_PASSWORD "myPassword"
#define WEBHOOK_URL "example.com/aoeuhtns" #define WEBHOOK_URL "example.com/aoeuhtns"
@ -8,12 +12,58 @@
// //
// We would expect the webhook to return something like: // We would expect the webhook to return something like:
// //
// [1 0 1] // [1, 0, 1]
// //
// to activate the momentary switch, (pin 5) deactivate the first // to activate the momentary switch, (pin 5) deactivate the first
// latched switch, (pin 8) and activate the second latched switch. (pin 10) // latched switch, (pin 8) and activate the second latched switch. (pin 10)
const int PIN_MAP[][2] = { const int NUM_PINS = 3;
const int PIN_MAP[NUM_PINS][2] = {
{5, 1}, {5, 1},
{8, 1}, {8, 1},
{10, 1} {10, 1}
}; };
// The signing/root certificate for your webhook site.
// For internal installations this might be a self-signing cert.
// For public installations, acquire the root cert for the CA and
// place it here. You can get this for your target domain with:
//
// sudo openssl s_client -connect sss.annabunch.es:443 -showcerts
//
// Be sure that you copy the cert that is described as a "Root CA" or
// "Root Trust" certificate.
//
// The provided example is for letsencrypt.org
static const char ca_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----
)EOF";
// Do not edit this line
#endif

View File

@ -1,6 +1,5 @@
#include "config.h" #include "config.h"
#include <ESP8266WiFi.h> #include "wifi.h"
#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
// how long to delay between each request to the // how long to delay between each request to the
@ -18,96 +17,52 @@ const int OLD_HIGH = 255;
// Holds the current state of all the pins. // Holds the current state of all the pins.
// Gets written to by parse_webhook_response(). // Gets written to by parse_webhook_response().
// Gets read from by set_pins(). // Gets read from by set_pins().
int pin_states[sizeof(PIN_MAP)] = {0}; int pin_states[NUM_PINS] = {0};
// Just a static client object to avoid memory allocations.
HTTPClient client;
void init_serial() {
Serial.begin(9600);
}
void init_wifi() {
Serial.println("Attempting to (re)connect to wifi");
WiFi.disconnect();
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int elapsed = 0;
while (WiFi.status() != WL_CONNECTED) {
Serial.print("Wifi connecting for ");
Serial.print(elapsed);
Serial.println(" seconds");
elapsed++;
delay(1000);
}
Serial.println("Wifi connected");
}
void setup() {
init_serial();
init_wifi();
}
void loop() {
poll_server();
set_pins();
int elapsed = handle_momentary();
Serial.flush();
delay(REQUEST_RATE - elapsed);
}
// if any momentary pins went high this loop, we // if any momentary pins went high this loop, we
// wait the agreed upon delay and then turn them off. // wait the agreed upon delay and then turn them off.
// if any momentary data input went low, we "reset" // if any momentary data input went low, we "reset"
// the pin. // the pin.
//
// returns the time spent delaying, if any.
int handle_momentary() { int handle_momentary() {
for (int i = 0; i < sizeof(pin_states); i++) { bool need_delay = false;
for (int i = 0; i < NUM_PINS; i++) {
// if [the pin is momentary] and [it was set high this cycle] // if [the pin is momentary] and [it was set high this cycle]
if (PIN_MAP[i][1] == 0 && pin_states[i] == HIGH) { if (PIN_MAP[i][1] == 0 && pin_states[i] == HIGH) {
// wait for the specified amount of time, then set the pin need_delay = true;
// back to low. break;
}
}
if (!need_delay) {
return 0;
}
delay(MOMENTARY_PERIOD); delay(MOMENTARY_PERIOD);
for (int i = 0; i < NUM_PINS; i++) {
// if [the pin is momentary] and [it was set high this cycle]
if (PIN_MAP[i][1] == 0 && pin_states[i] == HIGH) {
// set the pin back to low and make it 'sticky' until we read a low again.
digitalWrite(PIN_MAP[i][0], LOW); digitalWrite(PIN_MAP[i][0], LOW);
pin_states[i] = OLD_HIGH; pin_states[i] = OLD_HIGH;
} }
} }
return MOMENTARY_PERIOD;
} }
// poll_server makes the actual HTTP request and handles // poll_server makes the actual HTTP request and handles
// the result. It returns false if an error occurred. // the result. It returns false if an error occurred.
bool poll_server() { bool poll_server() {
client.begin(WEBHOOK_URL); String data = FetchURL(WEBHOOK_URL);
int status = client.GET(); if (data == "") return false;
if (status < 0) { parse_webhook_response(data);
Serial.print("Client error communicating with server: "); return true;
Serial.println(status);
return false;
}
if (status <= 400) {
Serial.print("Received HTTP status code ");
Serial.println(status);
return false;
}
parse_webhook_response(client.getString());
} }
void parse_webhook_response(String raw_data) { void parse_webhook_response(String raw_data) {
// int data[sizeof(PIN_MAP)] = {-1};
// // TODO: split the data and turn it into numbers
// while (raw_data.length() > 0) {
// // read to a newline
// String line = raw_data.substring(0, raw_data.indexOf('\n'));
// raw_data.remove(0, raw_data.indexOf('\n') + 1);
// // extract data from the line and add it to our incoming data array
// int index = line.substring(0, raw_data.indexOf(' ')).toInt();
// int state = line.substring(raw_data.indexOf(' ')+1).toInt();
// data[index] = state;
// }
StaticJsonDocument<256> doc; StaticJsonDocument<256> doc;
DeserializationError err = deserializeJson(doc, raw_data); DeserializationError err = deserializeJson(doc, raw_data);
@ -127,14 +82,24 @@ void parse_webhook_response(String raw_data) {
i++; i++;
} }
if (data.size() < sizeof(pin_states) / sizeof(*pin_states)) { if (data.size() < NUM_PINS) {
Serial.println("Did not receive data for all pins."); Serial.println("Did not receive data for all pins.");
} }
} }
// Set the pins to the right state, and set them all to high during
// initialization.
void initPins() {
for (int i = 0; i < NUM_PINS; i++) {
pinMode(PIN_MAP[i][0], OUTPUT);
digitalWrite(PIN_MAP[i][0], HIGH);
}
}
// Uses the current pin_states to actually write to the pins // Uses the current pin_states to actually write to the pins
void set_pins() { void writePins() {
for (int i = 0; i < sizeof(PIN_MAP); i++) { for (int i = 0; i < NUM_PINS; i++) {
if (tripped(i)) { if (tripped(i)) {
continue; continue;
} }
@ -149,3 +114,18 @@ void set_pins() {
bool tripped(int i) { bool tripped(int i) {
return PIN_MAP[i][1] == 0 && pin_states[i] == OLD_HIGH; return PIN_MAP[i][1] == 0 && pin_states[i] == OLD_HIGH;
} }
void setup() {
Serial.begin(9600);
initPins();
InitWifi();
}
void loop() {
poll_server();
writePins();
int elapsed = handle_momentary();
Serial.flush();
delay(REQUEST_RATE - elapsed);
}

View File

@ -1,13 +1,14 @@
# Control I/O pins with a webhook # Control I/O pins with a webhook
This is an Arduino IDE sketch for a "smart" controller that can activate pins This is an Arduino IDE sketch for a "smart" controller that can activate pins
based on the state of some webpage. The motivating use cases are: based on the state of some webpage. (designed to be driven by [gpio webhook server](https://git.annabunch.es/annabunches/gpio-webhook-server))
The motivating use cases are:
* Controlling a PC power switch remotely, using a transistor wired to the power switch pins * Controlling a PC power switch remotely, using a transistor wired to the power switch pins
* Lighting specific LEDs to create a remotely controlled 'traffic light'. * Lighting specific LEDs to create a remotely controlled 'traffic light'.
This sketch currently targets only the ESP8266, and will probably not work with other This sketch currently targets only the ESP8266, and will probably not work with other
microcontrollers. Support for other boards may come if I run out of ESP8266's. microcontrollers. Support for other boards may come if I run out of ESP8266s.
## Configuration ## Configuration
@ -37,7 +38,7 @@ only be high for a short time. 1 is for latched mode; the pin will stay high unt
## Building ## Building
**TODO** Just run `make` to build the code. You need to have `config.h` and `data/ca_cert.pem` defined.
## Webhook data ## Webhook data
@ -49,3 +50,9 @@ The webhook should always return a page in the following (JSON-compatible) forma
Where index and state are both integers. If you are expecting momentary input, you should return the Where index and state are both integers. If you are expecting momentary input, you should return the
state to '0' after the page is served / the webhook is consumed. state to '0' after the page is served / the webhook is consumed.
## Future Improvements
* make it actually work...
* use WifiManager?

69
wifi.h Normal file
View File

@ -0,0 +1,69 @@
#include "config.h"
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
HTTPClient client;
BearSSL::WiFiClientSecure *secure_transport;
BearSSL::Session *tls_session;
BearSSL::X509List cert_list;
void syncTime() {
// sync time
Serial.print("Syncing time");
configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov");
time_t now = time(nullptr);
while (now < 8 * 3600 * 2) {
delay(500);
Serial.print(".");
now = time(nullptr);
}
Serial.println("");
}
void initTLS() {
secure_transport = new BearSSL::WiFiClientSecure();
syncTime();
cert_list.append(ca_cert);
secure_transport->setTrustAnchors(&cert_list);
tls_session = new BearSSL::Session();
secure_transport->setSession(tls_session);
}
void InitWifi() {
Serial.println("Attempting to connect to wifi");
WiFi.disconnect();
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int elapsed = 0;
while (WiFi.status() != WL_CONNECTED) {
Serial.print("Wifi connecting for ");
Serial.print(elapsed);
Serial.println(" seconds");
elapsed++;
delay(1000);
}
Serial.println("Wifi connected");
initTLS();
}
String FetchURL(const char *path) {
client.begin(*secure_transport, WEBHOOK_URL);
int status = client.GET();
if (status < 0) {
Serial.print("Client error communicating with server: ");
Serial.println(status);
return "";
}
if (status >= 400) {
Serial.print("Received HTTP status code ");
Serial.println(status);
return "";
}
return client.getString();
}