Skip to content

Instantly share code, notes, and snippets.

@savetheclocktower
Last active August 12, 2018 21:38
Show Gist options
  • Save savetheclocktower/40ee61fc40aad5b2259b5e316e29f453 to your computer and use it in GitHub Desktop.
Save savetheclocktower/40ee61fc40aad5b2259b5e316e29f453 to your computer and use it in GitHub Desktop.
Laundry Spy firmware
// (see the blog post at https://andrewdupont.net/2018/04/27/laundry-spy-part-3-the-software/)
// GENERAL CONFIG
// ==============
// The baud rate of serial output for logging. If necessary, change the baud
// rate in your Serial Monitor to match this.
#define BAUD_RATE 115200
// The name by which this device will identify itself over mDNS (Bonjour).
// Easier to work with than an IP address.
#define HOST "laundry-spy"
// WASHER/DRYER CONFIG
// ===================
// Vibration threshold necessary to change from IDLE to MAYBE_ON. You might
// need to adjust these values depending on how much your washer and dryer
// vibrate.
#define WASHER_THRESHOLD 0.20
#define DRYER_THRESHOLD 0.20
// How often a machine's vibration score should exceed the threshold to be
// considered ON. If we think a machine is on, but it goes at least TIME_WINDOW
// ms without exceeding the threshold, we'll decide it was a false alarm.
#define TIME_WINDOW 3000
// How long a machine needs to spend (in ms) in each of the MAYBE states until
// we move it to the next state. These should always be more than 3000ms.
#define TIME_UNTIL_ON 30000 // 30 seconds
#define TIME_UNTIL_DONE 300000 // 5 minutes
// WIFI CONFIG
// ===========
#define WLAN_SSID "your-ssid-goes-here"
#define WLAN_PASS "your-password-goes-here"
// MQTT CONFIG
// ===========
#define MQTT_SERVER "999.999.999.999"
#define MQTT_SERVER_PORT 1883
#define MQTT_USERNAME "your-mqtt-server-username"
#define MQTT_PASSWORD "your-mqtt-server-password"
#define MQTT_CONN_KEEPALIVE 300
// SENSOR CONFIG
// =============
// The I2C addresses of the accelerometers. It doesn't matter which plug goes
// into which port, since they have unique IDs. Just keep track of which sensor
// is attached to which machine.
#define ACCEL_I2C_ADDRESS_WASHER 0x19
#define ACCEL_I2C_ADDRESS_DRYER 0x18
// END CONFIG
#include <math.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <PubSubClient.h>
#include <SimpleTimer.h>
#include <WiFiUdp.h>
#include <SparkFunLIS3DH.h>
void accel_setup(LIS3DH accel) {
accel.settings.accelSampleRate = 50;
accel.settings.accelRange = 2;
accel.settings.adcEnabled = 1;
accel.settings.tempEnabled = 0;
accel.settings.xAccelEnabled = 1;
accel.settings.yAccelEnabled = 1;
accel.settings.zAccelEnabled = 1;
accel.begin();
}
// MQTT
// ====
const char WASHER_FEED[] = HOST "/washer/state";
const char DRYER_FEED[] = HOST "/dryer/state";
const char WASHER_FORCE_FEED[] = HOST "/washer/force";
const char DRYER_FORCE_FEED[] = HOST "/dryer/force";
// Temporary string to hold values that we're publishing via MQTT.
char tempStateValue[2];
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
void MQTT_connect() {
Serial.println("Connecting to MQTT...");
mqtt.setServer(MQTT_SERVER, MQTT_SERVER_PORT);
bool connected = false;
while (!connected) {
connected = mqtt.connect(HOST, MQTT_USERNAME, MQTT_PASSWORD);
if (!connected) {
Serial.println("Couldn't connect to MQTT. Retrying in 10 seconds...");
mqtt.disconnect();
delay(10000);
}
}
Serial.println("MQTT connected!");
}
void MQTT_handle() {
if ( !mqtt.connected() ) {
MQTT_connect();
}
mqtt.loop();
}
SimpleTimer timer;
enum ApplianceState {
// Nothing is happening.
IDLE,
// We had a vibration event. We think the appliance may be on, but we're
// not sure yet.
MAYBE_ON,
// Enough vibration has happened in a short time that we're ready to
// proclaim that the appliance is on.
ON,
// It stopped moving. Maybe it's done?
MAYBE_DONE,
// It's been silent for long enough that we're sure it's done.
DONE
};
class Appliance {
private:
LIS3DH accel;
// Last time that we were in the idle state.
long lastIdleTime = 0;
// Last time that the force exceeded our threshold, regardless of state.
long lastActiveTime;
// Last vibration score.
float force = 0.0;
// The score above which we should move the machine from IDLE to MAYBE_ON.
float threshold;
// The initial readings we got for acceleration along each axis.
float initialX;
float initialY;
float initialZ;
// The most recent acceleration values along each axis.
float lastX;
float lastY;
float lastZ;
void readAccelerometer() {
float total = 0;
lastX = accel.readFloatAccelX();
lastY = accel.readFloatAccelY();
lastZ = accel.readFloatAccelZ();
total += fabs(lastX - initialX);
total += fabs(lastY - initialY);
total += fabs(lastZ - initialZ);
force = total;
}
public:
// The appliance we're dealing with (either "Washer" or "Dryer").
String name;
// The feed we're publishing our state to.
String feedNameState;
// The feed we're publishing force values to.
String feedNameForce;
ApplianceState state;
Appliance (String n, LIS3DH a, float t, String fns, String fnf) : accel(a) {
name = n;
threshold = t;
state = IDLE;
feedNameState = fns;
feedNameForce = fnf;
}
void setup () {
// Set this at startup to ensure that a machine starts in the IDLE state.
lastActiveTime = millis() - (TIME_WINDOW + 100);
accel_setup(accel);
// Take readings at startup.
initialX = accel.readFloatAccelX();
initialY = accel.readFloatAccelY();
initialZ = accel.readFloatAccelZ();
}
void update () {
readAccelerometer();
long now = millis();
if (force > threshold) {
lastActiveTime = now;
}
// "Recently" active means our force exceeded the threshold at least once
// within the last three seconds.
bool wasRecentlyActive = (now - lastActiveTime) < TIME_WINDOW;
// This is the logic that navigates us through the state machine.
// There's IDLE, ON, and DONE, which are obvious. The MAYBE_ON and
// MAYBE_DONE states are the only ones from which we can move either
// forward or backward. Once we go to ON, there's no way to go back
// to IDLE.
switch (state) {
case IDLE:
if (wasRecentlyActive) {
// Whenever there's so much as a twitch, we switch to the MAYBE_ON
// state.
setState(MAYBE_ON);
} else {
lastIdleTime = now;
}
break;
case MAYBE_ON:
if (wasRecentlyActive) {
// How long have we been active?
if (now > (lastIdleTime + TIME_UNTIL_ON)) {
// Long enough that this is not a false alarm.
setState(ON);
} else {
// Let's wait a bit longer before we act.
}
} else {
// We're not active, meaning there's been no vibration for a few
// seconds. False alarm!
setState(IDLE);
}
break;
case ON:
if (wasRecentlyActive) {
// We expect to be vibrating and we are. All is well. Do nothing.
} else {
// We stopped vibrating. Are we off? Switch to MAYBE_DONE so we can
// figure it out.
setState(MAYBE_DONE);
}
break;
case MAYBE_DONE:
if (wasRecentlyActive) {
// We thought we were done, but we're vibrating now. False alarm!
// Go back to ON.
setState(ON);
} else if (now > (lastActiveTime + TIME_UNTIL_DONE)) {
// We've been idle for long enough that we're certain that the
// cycle has stopped.
setState(DONE);
}
break;
case DONE:
// Once we get to DONE, there's nothing to do except go back to the
// initial IDLE state.
setState(IDLE);
break;
}
}
bool publishForce () {
Serial.print(name);
Serial.print(" publishing force: ");
Serial.println(force);
// Arduino's sprintf doesn't support floats.
bool published = mqtt.publish(
feedNameForce.c_str(),
String(force).c_str()
);
if (!published) {
Serial.println(" ...couldn't publish!");
}
return published;
}
bool publishState () {
sprintf(tempStateValue, "%d", state);
Serial.print(name);
Serial.print(" publishing state: ");
Serial.println(state);
bool published = mqtt.publish(
feedNameState.c_str(),
tempStateValue,
true
);
if (!published) {
Serial.println(" ...couldn't publish!");
}
return published;
}
void setState(ApplianceState s) {
state = s;
publishState();
}
};
LIS3DH accelWasher(I2C_MODE, ACCEL_I2C_ADDRESS_WASHER);
LIS3DH accelDryer(I2C_MODE, ACCEL_I2C_ADDRESS_DRYER);
Appliance washer(
"Washer",
accelWasher,
WASHER_THRESHOLD,
WASHER_FEED,
WASHER_FORCE_FEED
);
Appliance dryer(
"Dryer",
accelDryer,
DRYER_THRESHOLD,
DRYER_FEED,
DRYER_FORCE_FEED
);
bool shouldPublishState = false;
void schedulePublishState() {
shouldPublishState = true;
}
void publishState() {
washer.publishState();
dryer.publishState();
}
bool shouldPublishForce = false;
void schedulePublishForce() {
shouldPublishForce = true;
}
void publishForce() {
washer.publishForce();
dryer.publishForce();
}
void setup() {
Serial.begin(BAUD_RATE);
delay(10);
WiFi.mode(WIFI_STA);
WiFi.begin(WLAN_SSID, WLAN_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println("WiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
if ( !MDNS.begin(HOST) ) {
Serial.print("Error! Failed to broadcast host via MDNS: ");
Serial.println(HOST);
delay(5000);
ESP.restart();
}
washer.setup();
dryer.setup();
// Publish to the MQTT feed every five minutes whether we've got a new state
// or not.
timer.setInterval(300000, schedulePublishState);
// Publish the most recent force reading every so often. This is useful for
// determining a good threshold.
timer.setInterval(2000, schedulePublishForce);
Serial.println("Ready!");
}
void loop() {
timer.run();
MQTT_handle();
if (shouldPublishState) {
publishState();
shouldPublishState = false;
}
if (shouldPublishForce) {
publishForce();
shouldPublishForce = false;
}
washer.update();
dryer.update();
delay(50);
}
@dhowdy
Copy link

dhowdy commented Jun 16, 2018

After further review, I can confirm that you are precisely correct in that the initial values do not seem to matter and there is no real need for them to start close to 0.00. In my case, however, my accelerometers seem to give initial values of exactly 0.00 and then the next values are around 3.50 and then kind of stick at 0.81 or 1.79 when they are not working correctly; if they don't essentially zero out at boot and stay near zero then they sometimes give goofy data and I have to reset them. I'm not sure what's going on with them but I'm pretty sure it has nothing to do with the code.

Thank you again for sharing your code. And I'll continue to contribute back any changes I make to my own setup in case anyone else finds value in it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment