Compare commits
4 Commits
fcbf9a046a
...
main
Author | SHA1 | Date | |
---|---|---|---|
42a3835b43 | |||
e74fe369e5 | |||
3569508c88 | |||
bca75ff5e6 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
config.h
|
config.h
|
||||||
*.bin
|
*.bin
|
||||||
*.elf
|
*.elf
|
||||||
|
configs/
|
||||||
|
4
Makefile
4
Makefile
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
13
readme.md
13
readme.md
@ -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
69
wifi.h
Normal 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();
|
||||||
|
}
|
Reference in New Issue
Block a user