-
-
Save astromahi/a8f3dc0944e37e5eb2dd to your computer and use it in GitHub Desktop.
Brad's xmas lights
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 [email protected]:xmas.tmp | |
// ssh [email protected] '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