The Sunrise Machine

The Sunrise Machine

What's That, Now?

  • I dreamed it up in Portland, Ore.
  • A machine to automatically film the sunrise for you

How's it Work?

  • USB webcam
  • USB thumb drive
  • Pushbutton, RGB LED
  • JavaScript
  • Johnny-Five!

How's it Work?

  • `config.js`
  • `av.js`
  • `tweet.js`
  • `sunriseMachine.js`
  • `Recording.js`
module.exports = {
  autoMode           : 'sunrise', // Any property from `suncalc` or `false` to disable
  lat                : 43.34, // Your latitude (This is Vermont)
  long               : -72.64, // Your longitude (This is Vermont)
  postToTwitter      : false, // `true` will create posts of GIFs
  tweetBody          : 'It is a beautiful day!', // Text tweeted with GIFs
  captureFrequency   : 30000, // in ms (default one image every 30s)
  captureCount       : 20, // number of stills per montage
  calibrateCamera    : true, // calibrate camera exposure before shots
  calibrateLength    : 3000, // in ms (default 3 seconds)
  basedir            : '/mnt/sda1/captures', // where files get stored
  standbyColor       : '#0000ff', // LED color when no scheduled recording
  scheduledColor     : '#00ff00', // LED color when auto-recording is scheduled
  recordingColor     : '#ff0000', // LED color when recording session active
  consumer_key       : '', // Twitter
  consumer_secret    : '', // Twitter
  access_token_key   : '', // Twitter
  access_token_secret: '' // Twitter
};

Config

Default Behavior

  • Auto-film the sunrise
  • No Tweeting

Sun-Event Calculation

{
  autoMode           : 'sunrise', // Any property from `suncalc` or `false` to disable
  lat                : 43.34, // Your latitude (This is Vermont)
  long               : -72.64, // Your longitude (This is Vermont)
  /* ... */
}

Tweeting

{
  postToTwitter      : false, // `true` will create posts of GIFs
  tweetBody          : 'It is a beautiful day!', // Text tweeted with GIFs
  consumer_key       : '', // Twitter
  consumer_secret    : '', // Twitter
  access_token_key   : '', // Twitter
  access_token_secret: '' // Twitter
  /* ... */
}

Making Films

{
  captureFrequency   : 30000, // in ms (default one image every 30s)
  captureCount       : 20, // number of stills per montage
  calibrateCamera    : true, // calibrate camera exposure before shots
  calibrateLength    : 3000, // in ms (default 3 seconds)
  basedir            : '/mnt/sda1/captures', // where files get stored
  /* ... */
}

It's coming out all white

Making Films: The Nitty-Gritty

  • Tessel 2: 64MB RAM
  • Max resolution of capture: 320x240
  • Following in the intelligent footsteps of @rwaldron and `tessel-av`
  • Spawning child processes of `ffmpeg`

Tweeting Films: The Journey

  • npm `twitter` module capable of supporting Twitter's Media API, but no precedent
  • Bug in `twitter` (now fixed! Thanks @reconbot) considered non-200 HTTP 2xx responses as errors
  • Uploading videos to Twitter is 3-step process
  • And then...it turns out...

Tweeting Films: The Ugh

  • After all that, FINALIZE media/upload API call was returning 400 (Invalid Content)
  • Twitter will only accept h.264-encoded video
  • OpenWRT build of `ffmpeg` doesn't have an h.264 encoder
  • Now what?!

Animated GIFs, that's wut.

  • `ffmpeg` can create animated GIFs
  • Twitter will take animated GIFs
  • But then...

Oh, for goodness' sake

<--- Last few GCs --->

   76889 ms: Mark-sweep 10.1 (18.5) -> 9.6 (18.5) MB, 431.5 / 0 ms [allocation failure] [GC in old space requested].
   77308 ms: Mark-sweep 9.8 (18.5) -> 9.3 (18.5) MB, 418.4 / 0 ms [allocation failure] [GC in old space requested].
   77728 ms: Mark-sweep 9.5 (18.5) -> 9.3 (18.5) MB, 419.8 / 0 ms [last resort gc].
   78122 ms: Mark-sweep 9.3 (18.5) -> 8.9 (18.5) MB, 393.8 / 0 ms [last resort gc].


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x4ac24d59 <JS Object>
    1: /* anonymous */(aka /* anonymous */) [vm.js:39] [pc=0x5cbc09dc] (this=0x4ac08099 <undefined>,code=0x3c1598f1 <String[5]: Debug>)
    2: ensureDebugIsInitialized(aka ensureDebugIsInitialized) [util.js:194] [pc=0x5cbc07d4] (this=0x4ac08099 <undefined>)
    3: inspectPromise(aka inspectPromise) [util.js:200] [pc=0x5cbc0304] (this=0x4ac08099 <undefined>,p=0x3aaa675d <an Object with map 0x4a08333...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory
Aborted

This is where it got hairy

  • I assumed (wrongly) that `ffmpeg` was killing memory
  • Then I assumed (wrongly) that uploading the file all in one chunk was killing memory
  • Then I thought maybe some nested Promise was breaking everything...
  • So I refactored all of my Twitter API code to be terrible...

Memory Crash

  • Comes from logging `response` or `error` objects
  • That means I can't log `error`s as they come back from `twitter` directly.
  • So I refactored back to Promises.

ffmpeg

makes me feel dum

For some it's so easy

  • Argument order matters
  • There about fifty hundred things it can do
  • It's very powerful
  • It's very complicated

USB Cameras: The reckoning

/**
 * "Calibrate" a camera for a few seconds by letting it output to /dev/null
 */
module.exports.calibrate = function () {
  // ffmpeg -y -input_format yuyv422 -r 15 -i /dev/video0 -vframes 45 -f mp4 /dev/null
  var videoArgs = [];
  videoArgs.push('-y');
  videoArgs.push('-s', '320x240'); // Dimensions, respecting limited T2 memory
  videoArgs.push('-input_format', 'yuyv422'); // Native raw video format
  videoArgs.push('-r', '15'); // framerate
  videoArgs.push('-i', '/dev/video0'); // path to USB camera
  videoArgs.push('-vframes', '45'); // total frames to capture
  videoArgs.push('-f', 'mp4'); // Output format
  return ffmpeg(videoArgs, '/dev/null');
};

Capturing Stills

/**
 * Capture a single still image at 320x240 from the attached USB camera
 */
module.exports.captureStill = function (filepath) {
  // ffmpeg -y -v fatal -i /dev/video0 -s 320x240 -r 15 -q:v 2 -vframes 1 <filepath>
  var captureArgs = [];
  captureArgs.push('-s', '320x240'); // Dimensions, respecting limited T2 memory
  captureArgs.push('-r', '15'); // path to USB camera
  captureArgs.push('-i', '/dev/video0'); // path to USB camera
  captureArgs.push('-q:v', '2'); // JPG quality (1-31 where 1 is best)
  captureArgs.push('-vframes', '1'); // Total number of frames to capture
  return ffmpeg(captureArgs, filepath);
};

Making Movies

/**
 * Build an MP4 video from a collection of JPGs indicated by `glob`
 */
module.exports.videoFromStills = function (glob, outfile) {
  // ffmpeg -y -v fatal -s 320x240 -c:v h264 -f image2 -pattern_type \
  //   glob -framerate 6 -i capture*.jpg movie.mp4
  var stillArgs = [];
  stillArgs.push('-s', '320x240'); // Dimensions, respecting limited T2 memory
  stillArgs.push('-f', 'image2'); // FROM this input format...
  stillArgs.push('-pattern_type', 'glob'); // Use a glob to identify input
  stillArgs.push('-framerate', 6); // Framerate/frames per second
  stillArgs.push('-i', glob); // use this glob to find input files
  return ffmpeg(stillArgs, outfile);
};

Making GIFs

/**
 * Create an animated GIF from an MP4 video.
 */
module.exports.animatedGIFFromVideo = function (videofile, outfile) {
  // ffmpeg  -i video.mp4 -vf fps=6,scale=320:-1:flags=lanczos,palettegen palette.png
  var paletteArgs = [];
  paletteArgs.push('-y');
  paletteArgs.push('-i', videofile);
  paletteArgs.push('-vf');
  paletteArgs.push('fps=6,scale=320:-1:flags=lanczos,palettegen');
  return ffmpeg(paletteArgs, '/tmp/palette.png').then(paletteFile => {
    // ffmpeg -i video.mp4 -i palette.png -lavfi paletteuse output.gif
    console.log('made it back with ', paletteFile);
    var gifArgs = [];
    gifArgs.push('-y');
    gifArgs.push('-i', videofile);
    gifArgs.push('-i', paletteFile);
    gifArgs.push('-lavfi', 'paletteuse');
    return ffmpeg(gifArgs, outfile);
  });
};

Enclosure

  • Finding an old book to sacrifice
  • I'm terrible at soldering
  • Didn't use long enough wires
  • Coiling USB cable from camera atop T2 harshes WiFi reception

A speed prototype!

A speed prototype!

What's next.

The Sunrise Machine

The Sunrise Machine

By lyzadanger

The Sunrise Machine

  • 663