Display Your Productivity on Your Wall
October 15, 2022
Summary
I'm currently my own manager, so I wanted to give myself a little accountability. I made this device to display my productivity over the last 60 minutes using RescueTime, an IoT device built with Particle and NeoPixel, and a Raspberry Pi. I like how this turns my productivity into a little game, so I'm motivated to try to turn all the lights blue and keep it going.
RescueTime watches my PC and phone and keeps track of what I'm doing, labeling them as either productive or unproductive. It provides data in 5-minute chunks, which I can use to display on my circular NeoPixel Ring, kind of like a clock.
There are two main pieces of code for this mashup: The cron script and the Particle IoT device.
The Particle Photon With NeoPixel Ring
I used a Particle Photon and a NeoPixel Ring Kit for the hardware. I want this device to be able to handle many inputs, so I made a generic color/speed functions and other scripts can tell it what colors to show and at what speed.
Basically this script will set up Particle Cloud Functions for speed and colors, then regularly updates the lights based on the current speed and colors it has received. For this particular project, I don't want the lights to spin so I will keep the speed at 0.
// This #include statement was automatically added by the Particle IDE.
#include <neopixel.h>
// This #include statement was automatically added by the Particle IDE.
#include <elapsedMillis.h>
#include <math.h>
#define PIXEL_PIN D6
#define PIXEL_TYPE WS2812B
LEDSystemTheme theme; // Enable custom theme
int boardLed = D7;
elapsedMillis ledElapsed;
bool ledOn = false;
bool busy = false;
unsigned int ledInterval = 100;
uint8_t colorArray [72] = { };
int period = 2000;
const int PIXEL_COUNT = 24;
const float PI = 3.14159;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
int pos = 0, dir = 1;
int frame = 0;
int j;
void setup() {
pinMode(boardLed,OUTPUT); // Our on-board LED is output as well
ledElapsed = 0;
// Variables to be watched
Particle.function("colors", handleColorsUpdate);
Particle.function("speed", handleSpeedUpdate);
theme.setColor(LED_SIGNAL_CLOUD_CONNECTED, 0x00000000); // Set LED_SIGNAL_NETWORK_ON to no color
theme.apply(); // Apply theme settings
strip.begin();
strip.setBrightness(25); //set the brightness of NeoPixels to your preference
strip.show(); // Initialize all pixels to 'off'
clear();
}
void loop() {
if( busy && ledElapsed > ledInterval ) {
toggleLed();
}
updateStrip();
}
void updateStrip() {
if (period > 0) frame = (frame + 1) % strip.numPixels();
int brightness = 128;
for ( uint16_t p = 0; p < strip.numPixels(); p++ ) {
int index = (p + frame) % strip.numPixels();
strip.setPixelColor(
index,
strip.Color(
colorArray[p*3], // r
colorArray[p*3+1], // g
colorArray[p*3+2] // b
)
);
}
strip.show();
// delay( 16 ); // 60fps
if (period > 0) {
delay( (int)(period / strip.numPixels()) ); // Update when it's time to move to the next LED
} else {
delay( 1000 );
}
}
void clear() {
period = 0;
for ( uint16_t p = 0; p < strip.numPixels(); p++ ) {
colorArray[p*3] = 0;
colorArray[p*3+1] = (p % 3 == 0) ? 32 : 0;
colorArray[p*3+2] = 0;
}
}
void startLed() {
ledOn = true;
ledElapsed = 0;
digitalWrite(boardLed,HIGH);
}
void stopLed() {
digitalWrite(boardLed,LOW);
ledOn = false;
}
void toggleLed() {
if (ledOn == true) {
stopLed();
}
else {
startLed();
}
ledElapsed = 0;
}
int handleColorsUpdate(String command) {
// Must be 144 characters
// Like ff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ffff00ff
if (strlen(command) < strip.numPixels() * 6) return -1;
char colorData[strip.numPixels() * 6] = { };
command.toCharArray(colorData, strip.numPixels() * 6);
for (int i = 0; i < strip.numPixels(); i += 1) {
char red[2] = { };
command.substring(i*6, i*6+2).toCharArray(red, 2);
char green[2] = { };
command.substring(i*6+2, i*6+4).toCharArray(green, 2);
char blue[2] = { };
command.substring(i*6+4, i*6+6).toCharArray(blue, 2);
colorArray[i*3] = (uint8_t)strtol(red, NULL, 16);
colorArray[i*3+1] = (uint8_t)strtol(green, NULL, 16);
colorArray[i*3+2] = (uint8_t)strtol(blue, NULL, 16);
}
return 0;
}
int handleSpeedUpdate(String command) {
period = command.toInt();
return 0;
}
The Cron script
First I start a new project in a directory
mkdir rescuetime-ledring
cd rescuetime-ledring
npm init
npm i chalk cross-fetch dotenv
git init
and I add "type": "module",
to the package.json
, and add node_modules
and .env
to a .gitignore
file
Then I make a .env
file to hold a RescueTime API Key and the Particle Access Token
DEBUG=true
RT_KEY=XXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXX_XXXXXX
PARTICLE_ACCESS_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Now I'm ready to make the script itself
This will pull data from the RescueTime Analytic Data API, format it into a color string the Particle script I wrote expects, then sends it to the Particle Photon through the Particle Function.
import * as dotenv from 'dotenv'
dotenv.config()
import chalk from 'chalk'
import fetch from 'cross-fetch'
const DEBUG = process.env.DEBUG
const PARTICLE_ACCESS_TOKEN = process.env.PARTICLE_ACCESS_TOKEN
const RT_KEY = process.env.RT_KEY
// 24 lights, 6 hex per = 144 characters
const main = async () => {
// Get data from RescueTime
const productivity = await getRTFeed()
// Form a string of lights
const colorString = getColorString(productivity.rows)
// Send it to Particle
await updateColors(colorString)
}
const getRTFeed = async () => {
// To request information about the user's productivity levels, by hour, for the date of January 1, 2020:
// https://www.rescuetime.com/anapi/data
// ?key=YOUR_API_KEY
// &perspective=interval
// &restrict_kind=productivity
// &interval=hour
// &restrict_begin=2020-01-01
// &restrict_end=2020-01-01
// &format=json
const day = 1000 * 60 * 60 * 24 // milliseconds in a day
const today = new Date()
const yesterday = new Date(today - day)
const tomorrow = new Date(today + day)
const rtQuery = {
key: RT_KEY,
perspective: 'interval',
resolution_time: 'minute',
restrict_begin: `${yesterday.getUTCFullYear()}-${yesterday.getMonth() + 1}-${yesterday.getDate()}`,
restrict_end: `${tomorrow.getUTCFullYear()}-${tomorrow.getMonth() + 1}-${tomorrow.getDate()}`,
restrict_kind: 'productivity',
format: 'json'
}
const qs = new URLSearchParams(rtQuery)
const rtUrl = `https://www.rescuetime.com/anapi/data?${qs.toString()}`
const response = await fetch(rtUrl)
const json = await response.json()
return json
}
const getColorString = (productivity) => {
const now = new Date(productivity[productivity.length - 1][0])
const colors = new Array(24 * 3).fill(0x00) // 24 LEDs, 3 channels each
for (let i = 0; i < productivity.length; i++) {
const [date, seconds, num, prodScore] = productivity[i]
const d = new Date(date)
// If it's over 1 hour old, break
if (d <= new Date(now - 1000 * 60 * 55)) continue
const slot = Math.floor(d.getMinutes() / 5) * 2 * 3
const percent = seconds / 300
if (seconds > 0) {
if (prodScore < 0) {
// Increase red
colors[slot] += Math.floor(0xff * percent)
colors[slot + 3] += Math.floor(0xff * percent)
} else if (prodScore > 0) {
// Increase blue
colors[slot + 2] += Math.floor(0xff * percent)
colors[slot + 5] += Math.floor(0xff * percent)
} else {
// increase green
colors[slot + 1] += Math.floor(0xff * percent)
colors[slot + 4] += Math.floor(0xff * percent)
}
}
}
return colors.map((c, i) => {
return c.toString(16).padStart(2, '0')
}).join('')
}
const updateColors = async (colorString) => {
if (DEBUG) {
// Show a log of it
let lightString = ''
for (let i = 0; i < colorString.length; i = i + 6) {
const colorUnit = `#${colorString.substring(i, i + 6)}`
const unitColor = chalk.hex(colorUnit)
lightString += unitColor('⬤ ')
}
console.log(lightString)
}
const particleUrl = 'https://api.particle.io/v1/devices/3b003b000747343232363230/colors'
const response = await fetch(particleUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${PARTICLE_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
arg: colorString
})
})
const json = await response.json()
if (DEBUG) console.log(json)
}
main()
Making it Run regularly
The last step was to use a Raspberry Pi to run it every 5 minutes. I cloned the repository, added the .env information, then set up a cron job using PM2 to manage it. You could use the system's cron to handle it, but I am already using PM2 for other services so I chose to keep everything together.
pm2 start index.js --name rescuetime-ledring --cron "*/5 * * * *"
pm2 save
Source
You can see the full cron script on GitHub