Skip to content

Instantly share code, notes, and snippets.

@bradfitz
Created December 25, 2015 02:39
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bradfitz/a5d4fabe92a87de8025d to your computer and use it in GitHub Desktop.
Save bradfitz/a5d4fabe92a87de8025d to your computer and use it in GitHub Desktop.
Brad's xmas lights
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// --------------------------------------------------------------------------
// The xmas command runs Brad's Christmas lights.
//
// This is not code I'm very proud of. It's code I write with a glass
// or three of wine.
//
// Buy a Raspberry Pi and 1-5 of these:
// "Addressable LED Strip APA102-C"
// (5 meters, 30 LEDs/meter, White PCB, silicone sleeve)
// http://www.amazon.com/gp/product/B00YVYSOC2
//
// Then make sure you either patch this in or verify it's already
// fixed upstream: https://github.com/rakyll/experimental/issues/3
//
package main
import (
"flag"
"fmt"
"log"
"math"
"math/rand"
"os"
"strconv"
"time"
"github.com/rakyll/experimental/spi"
)
// NumLight is the number of lights in the strip.
// Fewer means better refresh rate.
const NumLight = 749
// CardDir is a cardinal direction.
// It is aspirational only for an unwritten simulator.
type CardDir byte
const (
_ CardDir = iota
North
South
East
West
)
// Line is a subset of the lights on Brad's house.
type Line struct {
Low, High int
Dir CardDir
}
var (
Wall = Line{22, 81, East} // back wall on deck
G1 = Line{82, 126, South} // east glass panel #1
G2 = Line{127, 167, South} // ...
G3 = Line{168, 212, South}
G4 = Line{213, 255, South}
G6 = Line{256, 299, South}
G7 = Line{300, 341, South}
G8 = Line{342, 385, South}
G9 = Line{386, 429, South} //east glass panel #9
G19 = Line{82, 429, South} // all east glass panels, 1-9
F5 = Line{430, 462, West} // rightmost glass panel when facing house (left side is high num)
F4 = Line{463, 501, West} // ...
F3 = Line{502, 538, West}
F2 = Line{539, 575, West}
F1 = Line{576, 612, West} // leftmost glass panel when facing house (left side is high num)
Front = Line{430, 612, West} // all front glass panels
GW = Line{613, 654, North} // glass part facing west by door
FW = Line{655, 705, West} // front wood (over door)
WoodW = Line{706, 748, North} // west wall
All = Line{22, 748, 0} // all useful lights (excluding clump at beginning)
)
func (ln *Line) Foreach(e *PaintEnv, fn func(p Pixel)) {
for i := ln.Low; i <= ln.High; i++ {
fn(Pixel{e, i})
}
}
const (
startBytes = 4
stopBytes = ((NumLight / 2) / 8) + 1
)
// Mem is the memory we send to the SPI device.
// It includes the 4 zero bytes and the variable number of
// 0xff stop bytes.
type Mem [4 + NumLight*4 + stopBytes]byte
// maxBright is the maximum brightness level (31).
// Each pixel can have brightness in range [0,31].
const maxBright = 0xff - 0xe0
// init populates the stop bytes with 0xff and sets
// everything else to black.
func (m *Mem) init() {
s := m[4+NumLight*4:]
for i := range s {
s[i] = 0xff
}
m.zero()
}
func (m *Mem) setLight(n int, r, g, b, a uint8) {
if a > maxBright {
panic("a too high; max is 31")
}
if n >= NumLight {
panic("n too big")
}
if n < 0 {
panic("negative n")
}
s := m[4+n*4:]
s[0] = 0xe0 + a
s[1] = b
s[2] = g
s[3] = r
}
func (m *Mem) zero() {
for i := 0; i < NumLight; i++ {
m.setLight(i, 0, 0, 0, 0)
}
}
func (m *Mem) send(d *spi.Device) error {
err := d.Do(m[:], 0)
return err
}
// dieWhenBinaryChanges exits the program when it detects the program
// changed. The Raspberry Pi is running:
//
// $ while true; do ./xmas ; done
//
// ... in a screen session. And my Makefile on my Mac has:
//
// .PHONY:
//
// xmas: .PHONY
// GOARM=6 GOOS=linux GOARCH=arm go install .
// scp -C /Users/bradfitz/bin/linux_arm/xmas pi@10.0.0.30:xmas.tmp
// ssh pi@10.0.0.30 'install xmas.tmp xmas'
//
// So I can just run "make" to updated the house lights within ~2 seconds.
func dieWhenBinaryChanges() {
fi, err := os.Stat(os.Args[0])
if err != nil {
log.Fatal(err)
}
mod0 := fi.ModTime()
for {
time.Sleep(500 * time.Millisecond)
if fi, err := os.Stat(os.Args[0]); err != nil || !fi.ModTime().Equal(mod0) {
log.Printf("modtime changed; dying")
os.Exit(1)
}
}
}
func main() {
flag.Parse()
go dieWhenBinaryChanges()
var m Mem
m.init()
d, err := spi.Open("/dev/spidev0.1")
if err != nil {
panic(err)
}
defer d.Close()
if err := d.SetMode(spi.Mode3); err != nil {
panic(err)
}
if err := d.SetSpeed(2000000); err != nil {
panic(err)
}
if err := d.SetBitsPerWord(8); err != nil {
panic(err)
}
log.Printf("num arg = %v", flag.NArg())
if flag.NArg() == 1 {
log.Printf("Debugging light %q", flag.Arg(0))
targ, _ := strconv.Atoi(flag.Arg(0))
m.setLight(targ, 255, 255, 255, maxBright)
if err := m.send(d); err != nil {
log.Fatal(err)
}
return
}
var anims = []Animation{
wreath{},
&randomSegs{},
randomWhite{},
snakeRedGreen{},
traditional{},
&fireworks{},
kippes{},
&candyCane{},
}
only := func(a Animation) {
anims = []Animation{a}
}
_ = only
//only(&fireworks{})
for {
for _, a := range anims {
e := &PaintEnv{
Mem: &m,
}
for {
a.Paint(e)
e.Cycle++
t := time.Now()
if err := m.send(d); err != nil {
log.Fatal(err)
}
d := time.Since(t)
log.Printf("took %v (%d), cycle %d", d, NumLight, e.Cycle)
if len(anims) > 1 && e.Cycle == 150 {
break
}
}
}
}
}
type Pixel struct {
e *PaintEnv
N int
}
func (x Pixel) set(r, g, b, a uint8) {
x.e.setLight(x.N, r, g, b, a)
}
type PaintEnv struct {
*Mem
Cycle int
}
type Animation interface {
Paint(*PaintEnv)
}
type snakeRedGreen struct{}
func (snakeRedGreen) Paint(e *PaintEnv) {
All.Foreach(e, func(p Pixel) {
pos := ((p.N - e.Cycle) / 25)
orig := pos
pos %= 2
if pos < 0 {
pos = -pos
}
var r, g, b uint8
switch pos {
case 0:
r = 0xff
case 1:
g = 0xff
default:
panic("not 0 or 1: " + fmt.Sprintf("%d %% 2 = %d", orig, pos))
}
a := (p.N - e.Cycle) % 25
if a < 0 {
a = -a
}
p.set(r, g, b, 6+byte(a))
})
}
// randomWhite is a shitty first cut at snow falling on a deep blue
// sky. It needs work.
type randomWhite struct{}
func (randomWhite) Paint(e *PaintEnv) {
if e.Cycle%2048 == 0 {
All.Foreach(e, func(p Pixel) {
p.set(0, 0, 128, maxBright)
})
}
for i := 0; i < 10; i++ {
n := rand.Intn(NumLight)
e.setLight(n, 254, 254, 254, maxBright/2)
}
for i := 0; i < 20; i++ {
n := rand.Intn(NumLight)
e.setLight(n, 254, 254, 254, maxBright)
}
for i := 0; i < 10; i++ {
n := rand.Intn(NumLight)
e.setLight(n, 0, 0, 128, maxBright)
}
}
// wreath is a dark green xmas wreath with lights alternating the
// traditional colors.
type wreath struct{}
func (wreath) Paint(e *PaintEnv) {
if e.Cycle == 0 {
All.Foreach(e, func(p Pixel) {
p.set(0, 90, 0, maxBright/7)
})
}
All.Foreach(e, func(p Pixel) {
if p.N%8 != 0 {
return
}
e := ((e.Cycle + p.N) / 10) % 4
if e < 0 {
e = -e
}
switch e {
case 0:
p.set(255, 0, 0, maxBright)
case 1:
p.set(255, 255, 0, maxBright)
case 2:
p.set(0, 0, 255, maxBright)
case 3:
p.set(255, 0, 255, maxBright)
}
})
}
// traditional tries to match the light pattern of the neighbors
// across the street.
type traditional struct{}
func (traditional) Paint(e *PaintEnv) {
const onWid = 2
const offWid = 5
const cols = 4
var col int
var bright uint8
All.Foreach(e, func(p Pixel) {
what := p.N % (onWid + offWid)
on := what < onWid
if on {
if what == 0 {
col++
col %= cols
bright = 10 + byte(rand.Intn(20))
}
switch col {
case 0:
p.set(255, 0, 0, bright)
case 1:
p.set(255, 200, 0, bright)
case 2:
p.set(0, 255, 0, bright)
case 3:
p.set(0, 0, 255, bright)
}
} else {
p.set(0, 0, 0, 0)
}
})
}
var segs = [...]Line{
G1, G2, G3, G4, G4, G6, G7, G8, G9,
F5, F4, F3, F2, F1,
GW, FW, WoodW,
}
// randomSegs makes each glass segment on the roof alternate between
// red and green at random times.
type randomSegs struct {
state []bool
count []int
}
func (a *randomSegs) Paint(e *PaintEnv) {
if e.Cycle == 0 {
a.state = make([]bool, len(segs))
a.count = make([]int, len(segs))
}
for i, seg := range segs {
on := a.state[i]
if a.count[i] == 0 {
a.state[i] = !on
a.count[i] = rand.Intn(20)
} else {
a.count[i]--
}
seg.Foreach(e, func(p Pixel) {
if on {
p.set(255, 0, 0, 30)
} else {
p.set(0, 128, 0, 20)
}
})
}
}
// HSLToRGB convert a Hue-Saturation-Lightness (HSL) color to sRGB.
// 0 <= H < 360,
// 0 <= S <= 1,
// 0 <= L <= 1.
// The output sRGB values are scaled between 0 and 1.
//
// This is a copy of https://code.google.com/p/chroma/source/browse/f64/colorspace/hsl.go#53
func HSLToRGB(h, s, l float64) (r, g, b float64) {
var c float64
if l <= 0.5 {
c = 2 * l * s
} else {
c = (2 - 2*l) * s
}
min := l - 0.5*c
h -= 360 * math.Floor(h/360)
h /= 60
x := c * (1 - math.Abs(h-2*math.Floor(h/2)-1))
switch int(math.Floor(h)) {
case 0:
r = min + c
g = min + x
b = min
break
case 1:
r = min + x
g = min + c
b = min
break
case 2:
r = min
g = min + c
b = min + x
break
case 3:
r = min
g = min + x
b = min + c
break
case 4:
r = min + x
g = min
b = min + c
break
case 5:
r = min + c
g = min
b = min + x
break
default:
r = 0
g = 0
b = 0
}
return
}
// kippes tries to match my eastward neighbor's light pattern.
type kippes struct{}
func (kippes) Paint(e *PaintEnv) {
All.Foreach(e, func(p Pixel) {
if p.N%2 == 1 {
p.set(0, 0, 0, 0)
return
}
l := 0.4
if rand.Intn(10) == 0 {
l = 0.6
}
s := 1.0
rf, gf, bf := HSLToRGB(25, s, l)
r, g, b := byte(rf*255), byte(gf*255), byte(bf*255)
var a byte = 10
p.set(r, g, b, a)
})
}
// candyCane is dark red with oscillating bright white segments around
// each bar on the roof.
type candyCane struct {
rad []float64
amp []float64
speed []float64
}
func (a *candyCane) Paint(e *PaintEnv) {
if e.Cycle == 0 {
a.rad = make([]float64, len(segs))
a.amp = make([]float64, len(segs))
a.speed = make([]float64, len(segs))
for i := range segs {
a.rad[i] = rand.Float64() * (math.Pi / 0.5)
a.amp[i] = float64(10 + rand.Intn(5))
a.speed[i] = 0.1 + (rand.Float64() / 3)
}
}
All.Foreach(e, func(p Pixel) {
p.set(90, 0, 0, maxBright/7)
})
for i, seg := range segs {
a.rad[i] += a.speed[i]
cos := math.Cos(a.rad[i]) * a.amp[i]
from := seg.Low
to := seg.Low + int(cos)
if to < from {
to, from = from, to
}
if from < 0 {
from = 0
}
if to >= NumLight {
to = NumLight
}
for n := from; n <= to; n++ {
e.setLight(n, 255, 255, 255, maxBright)
}
}
}
type particle struct {
amp float64
speedBump float64
}
// fireworks is exploding fireworks.
type fireworks struct {
origin int
lightInc float64
hue int // 0 to 360
lnx float64 // from 1.0 to 20.0 by 0.1
particles []*particle
light []float64
}
func (a *fireworks) Paint(e *PaintEnv) {
const maxLn = 10.0
const lnInc = 0.2
if a.lnx < 1 || a.lnx > maxLn {
a.origin = Front.Low + rand.Intn(Front.High-Front.Low)
a.hue = rand.Intn(360)
a.lnx = 1.0
a.particles = a.particles[:0]
a.lightInc = 0.05
for i := 0; i < 1000; i++ {
amp := (rand.Float64() - 0.5) * 2
amp *= 70
amp += math.Copysign(1, amp)
a.particles = append(a.particles, &particle{
amp: amp,
speedBump: 1.0 + rand.Float64()/6,
})
}
}
a.lnx += lnInc
a.lightInc -= 0.001
if a.lightInc < 0 {
a.lightInc = 0
}
a.light = a.light[:0]
for i := 0; i < NumLight; i++ {
a.light = append(a.light, 0.0)
}
for _, p := range a.particles {
x := float64(a.origin) + math.Log(a.lnx)*p.amp*p.speedBump
xi := int(x)
if xi < 0 || xi >= NumLight {
continue
}
a.light[xi] += a.lightInc
}
All.Foreach(e, func(p Pixel) {
l := a.light[p.N]
if l > 1 {
l = 1
}
s := 1.0
rf, gf, bf := HSLToRGB(float64(a.hue), s, l)
r, g, b := byte(rf*255), byte(gf*255), byte(bf*255)
p.set(r, g, b, byte(l*30))
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment