Things used in this project

Hardware components:
Photon new
Particle Photon
×1
Adafruit 0.8" 8x16 LED Matrix FeatherWing Display - Red
×1
Openbuilds wire cable by foot
OpenBuilds Wire Cable - By the Foot
×1
Hand tools and fabrication machines:
3drag
3D Printer (generic)
09507 01
Soldering iron (generic)

Custom parts and enclosures

Printed using only perimeters
Back lid

Schematics

Circuit
Always verify pinout before assembling your circuit!
Drop of life bb 4rwmw7cpim

Code

drop-of-life.inoArduino
/*
 * Drop of Life
 * Julien Vanier <jvanier@gmail.com>
 */

#include "Particle.h"
#include "HT16K33-LED.h"
#include <time.h>
#include <stdlib.h>

PRODUCT_ID(3599);
PRODUCT_VERSION(1);

SYSTEM_THREAD(ENABLED);

constexpr long minutesToMs(long minutes) { return minutes * 60 * 1000; }
constexpr long weekToSeconds(long week) { return week * 7 * 24 * 60 * 60; }

HT16K33 display;
const int ROWS = 16;
const int MAX_LEVEL = ROWS - 1;
const long ELIGIBILITY_UPDATE_INTERVAL = minutesToMs(60);
const long REDCROSS_DONATION_INTERVAL = weekToSeconds(8);

int level = 0;

void setup() {
  Serial.begin();
  setupStorage();
  setupDisplay();
}

void loop() {
  processCloud();
  processRedCross();
  processDisplay();
}

void processCloud() {
  static bool didConnect = false;
  if (!didConnect && Particle.connected()) {
    registerStorage();
    registerRedCross();
    registerDisplay();
    didConnect = true;
  }
}

/* Persistent storage in the EEPROM */

const uint16_t DROP_OF_LIFE_APP = ('D'<<8 | 'L');
struct Storage {
  uint16_t app;
  uint8_t username[32];
  uint8_t password[32];
} storage;

void setupStorage() {
  loadStorage();
}

void registerStorage() {
  Particle.function("login", setCredentials);
}

void loadStorage() {
  EEPROM.get(0, storage);
  if (storage.app != DROP_OF_LIFE_APP) {
    storage.app = DROP_OF_LIFE_APP;
    storage.username[0] = '\0';
    storage.password[0] = '\0';
    storeStorage();
  }
}

void storeStorage() {
  EEPROM.put(0, storage);
}

int setCredentials(String arg) {
  Serial.println("Set credentials");
  int comma = arg.indexOf(",");
  if (comma < 0) {
    return -1;
  }
  String username = arg.substring(0, comma);
  String password = arg.substring(comma + 1);
  username.getBytes(storage.username, sizeof(storage.username));
  password.getBytes(storage.password, sizeof(storage.password));
  storeStorage();
  return 0;
}

/* Red Cros API interactions */

String token;

#define EVENT_RC_LOGIN "red_cross/login"
#define EVENT_RC_ELIGIBILITY "red_cross/eligibility"

time_t eligibility = 0;

void registerRedCross() {
  Particle.subscribe(
    System.deviceID() + "/hook-response/" EVENT_RC_LOGIN,
    setRedCrossToken,
    MY_DEVICES
  );
  Particle.subscribe(
    System.deviceID() + "/hook-response/" EVENT_RC_ELIGIBILITY,
    setEligibility,
    MY_DEVICES
  );
}

void processRedCross() {
  static bool didLogin = false;
  static long lastUpdate = -ELIGIBILITY_UPDATE_INTERVAL;

  if (!Particle.connected()) {
    return;
  }

  if (!didLogin) {
    if (loginToRedCross()) {
      didLogin = true;
    }
  }

  if (didLogin && (millis() - lastUpdate > ELIGIBILITY_UPDATE_INTERVAL)) {
    if (updateEligibility()) {
      lastUpdate = millis();
    }
  }

  setLevelFromEligibility();
}

void setLevelFromEligibility() {
  long now = Time.now();
  if (eligibility != 0 && now != 0) {
    if (eligibility < now) {
      level = MAX_LEVEL;
    } else {
      level = MAX_LEVEL - ((eligibility - now) * MAX_LEVEL / REDCROSS_DONATION_INTERVAL);
    }
  }
}

void setRedCrossToken(const char *event, const char *data) {
  Serial.println("Got token");
  token = data;
}

bool loginToRedCross() {
  if (storage.username[0] == '\0' || storage.password[0] == '\0') {
    return false;
  }
  String data = String::format(
    "{\"username\":\"%s\",\"password\":\"%s\"}",
    storage.username,
    storage.password
  );
  Particle.publish(EVENT_RC_LOGIN, data, PRIVATE);
  return true;
}

bool updateEligibility() {
  if (token.length() == 0) {
    return false;
  }
  String data = String::format("{\"token\":\"%s\"}", token.c_str());
  Particle.publish(EVENT_RC_ELIGIBILITY, data, PRIVATE);
  return true;
}

void setEligibility(const char *event, const char *data) {
  Serial.println("Got eligibility date " + String(data));
  // convert string into time
  String dateStr = data;
  int dash1 = dateStr.indexOf("-");
  int dash2 = dateStr.indexOf("-", dash1+1);
  if (dash1 < 0 || dash2 < 0) {
    return;
  }
  int year = dateStr.substring(0, dash1).toInt();
  int month = dateStr.substring(dash1 + 1, dash2).toInt();
  int day = dateStr.substring(dash2 + 1).toInt();

  tm date;
  memset(&date, 0, sizeof(tm));
  date.tm_year = year - 1900;
  date.tm_mon = month - 1;
  date.tm_mday = day;

  eligibility = mktime(&date);
}

/* Display */

void setupDisplay() {
  display.begin();
}

void registerDisplay() {
  Particle.function("demo", startDemo);
}

const uint8_t DROP_FULL[ROWS] = {
  0b00010000,
  0b00010000,
  0b00111000,
  0b00111000,
  0b01111100,
  0b01111100,
  0b01111110,
  0b11111110,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b01111110,
  0b00111100,
};

const uint8_t DROP_EMPTY[ROWS] = {
  0b00010000,
  0b00010000,
  0b00101000,
  0b00101000,
  0b01000100,
  0b01000100,
  0b01000010,
  0b10000010,
  0b10000001,
  0b10000001,
  0b10000001,
  0b10000001,
  0b10000001,
  0b10000001,
  0b01000010,
  0b00111100,
};

const uint8_t LINE_TO_ROW[ROWS] = {
  15,
  13,
  11,
  9,
  7,
  5,
  3,
  1,
  14,
  12,
  10,
  8,
  6,
  4,
  2,
  0,
};

/* run demo once at startup */
int demoLevel = 0;
long demoTime = 0;

int startDemo(String) {
  demoLevel = 0;
  demoTime = millis();
  return 0;
}

void processDisplay() {
  if (demoLevel >= 0) {
    runDemo();
  } else {
    displayDrop(level);
  }
}

void runDemo() {
  static const long DEMO_DELAY = 1000;
  displayDrop(demoLevel);
  if (millis() - demoTime > DEMO_DELAY) {
    demoLevel++;
    demoTime = millis();
  }

  if (demoLevel >= MAX_LEVEL + 15) {
    demoLevel = -1;
  }
}

void displayDrop(uint8_t level) {
  uint8_t lines[ROWS];
  for (int i = 0; i < ROWS; i++) {
    uint8_t row = LINE_TO_ROW[i];
    lines[row] = (i < ROWS - level) ? DROP_EMPTY[i] : DROP_FULL[i];
  }
  display.writeDisplay(lines, 0, ROWS);
  updateBrightness(level);
}

void updateBrightness(uint8_t level) {
  static const int defaultBrightness = 9;
  static int brightness = 9;
  static const long FADE_INTERVAL = 300;
  static int fadeDirection = 1;
  static const int maxFadeBrightness = 14;
  static const int minFadeBrightness = 6;
  static long fadeTime = 0;

  if (level < MAX_LEVEL) {
    brightness = defaultBrightness;
  } else {
    if (millis() - fadeTime > FADE_INTERVAL) {
      brightness += fadeDirection;
      if (brightness >= maxFadeBrightness) {
        fadeDirection = -1;
      } else if (brightness <= minFadeBrightness) {
        fadeDirection = 1;
      }
      fadeTime = millis();
    }
  }
  display.setBrightness(brightness);
}
Photon code, Fritzing diagram and 3D models

Credits

Replications

Did you replicate this project? Share it!

I made one

Love this project? Think it could be improved? Tell us what you think!

Give feedback

Comments

Similar projects you might like

Lightweight Sliding Door Automator
Intermediate
  • 1,557
  • 8

Work in progress

Automate the opening and closing of a lightweight sliding door (e.g. a screen door), including remote controls on your phone.

Christmas Gift Box
Intermediate
  • 3,634
  • 595

Full instructions

Christmas Gift Box plays music and sends an email when it is opened.

Carbon Fiber Vacuum Chamber
Intermediate
  • 2,909
  • 94

Full instructions

Our project is a carbon fiber vacuum chamber that is monitored by multiple particle photons and various sensors.

ConnectTheDots with Particle Azure IoT Hub Integration
Intermediate
  • 844
  • 8

Protip

This project will allow you to connect your Particle device into an Azure IoT Hub for viewing data in real-time through an ASP.NET web app!

What should I wear outside?
Intermediate
  • 5,046
  • 67

Full instructions

A whimsical weather clock powered by Particle and forecast.io

LED Reflection Clock
Intermediate
  • 2,176
  • 35

Work in progress

Using 60 LED's facing a suitable wall, the time is projected in colour.

Add projectSign up / Login