package module
Version: v0.0.0-...-a5abf34 Latest Latest

This package is not in the latest version of its module.

Go to latest
Published: Apr 21, 2020 License: GPL-3.0 Imports: 15 Imported by: 0


Go Ether Dream

Go language interface to the Ether Dream laser DAC. Current features: blanking, basic path optimization, quality (trade resolution for frame rate) and 3D scene rendering via ln. For an introduction to laser projectors and programming, see: Laser Hack 101 presentation slides.

Based on the work of j4cbo, echelon and fogleman



  • projector/scanner - An ILDA compatible laser projector aka laser scanner. 2 (or more) mirrors, 2 galvos, some laser diodes and electronics. Not to be confused with the other kind of laser projector.
  • DAC - Digital to Analog Converter. An electronic box that translates digital signals from a computer into analog signals that control the galvos via an IDLA cable. There are proprietary and open source DACs, as well as modified sound cards used as DACs. In this context, it means your Ether Dream(s).


This assumes you are plugged in to your ether dream via ethernet cable. You may need to set up some rules for your firewall. Inbound communications are needed for the initial broadcast signal and handshake, if you don't need to Find a DAC, you can use outbound only.

  • Outbound rule: TCP port 7765
  • Inbound rule: UDP port 7654

The simplest setup involves one DAC and one projector, but there are many options.

  • Multiple projectors chained off one DAC signal
  • Multiple projectors chained off one DAC signal, including use of a cross-over ILDA cable to mirror left/right on one side of the room.
  • Multiple projectors each with their own DAC. This offers independent control over each projector. If calibrated in a stack, can be used to create complex imagery.


If you don't have Go installed, start here:

Once Go is installed with your environment updated, just:

go get
cd $GOPATH/src/

You can run any of the examples like:

go run examples/square/square.go
# if you aren't blocking the network ports, and your Ether Dream
# is connected to an ILDA laser, it should project a square


If you have opened the necessary ports, the Ether Dream will broadcast it's identity on the network. Once you have connected, you can provide a PointStream to play.

func main() {
    addr, bp, err := etherdream.FindFirstDAC()
    if err != nil {
        log.Fatalf("Network error: %v", err)

    log.Printf("Found DAC at %v\n", addr)
    log.Printf("BP: %v\n\n", bp)

    dac, err := etherdream.NewDAC(addr.IP.String())
    if err != nil {
    defer dac.Close()
    log.Printf("Initialized:  %v\n\n", dac.LastStatus)
    log.Printf("Firmware String: %v\n\n", dac.FirmwareString)

Point Streams

type PointStream func(w io.WriteCloser)

Point streams should contain an infinite loop that will use the WriteCloser interface to output encoded points to the DAC sequentially. In Ether Dream, a point has 2D vector information and a color (see: image/color).

// make a red point at X=0, Y=300
pt := etherdream.NewPoint(0, 300, color.RGBA{0xff, 0x00, 0x00, 0xff})

// Encode the point to bytes
by := pt.Encode()

// Stream the encoded points to the DAC

From examples\square\square.go:

func main() {
    debug := false
    dac.Play(squarePointStream, debug)
func squarePointStream(w io.WriteCloser) {
    defer w.Close()
    pmax := 15600
    pstep := 100
    for {
        for _, x := range xrange(-pmax, pmax, pstep) {
            w.Write(etherdream.NewPoint(x, pmax, color.RGBA{0xff, 0x00, 0x00, 0xff}).Encode())
        for _, y := range xrange(pmax, -pmax, -pstep) {
            w.Write(etherdream.NewPoint(pmax, y, color.RGBA{0x00, 0xff, 0x00, 0xff}).Encode())
        for _, x := range xrange(pmax, -pmax, -pstep) {
            w.Write(etherdream.NewPoint(x, -pmax, color.RGBA{0x00, 0x00, 0xff, 0xff}).Encode())
        for _, y := range xrange(-pmax, pmax, pstep) {
            w.Write(etherdream.NewPoint(-pmax, y, color.RGBA{0xff, 0xff, 0xff, 0xff}).Encode())

func xrange(min, max, step int) []int {
    rng := max - min
    ret := make([]int, rng/step+1)
    iY := 0
    for iX := min; rlogic(min, max, iX); iX += step {
        ret[iY] = iX
    return ret

func rlogic(min, max, iX int) bool {
    if min < max {
        return iX <= max
    return iX >= max


Etherdream library will intialize the following flags - use -help for more info:

-blank-count int
    How many samples to wait after drawing a blanking line. (default 20)
    Enable debug output.
-draw-speed float
    Draw speed (25-100). Lower is more precision but slower. (default 50)
-scan-rate int
    Number of points per second to play back. (default 24000)

Blanking and Paths

Here we introduce the use of tgreiser/ln, a fork of Fogleman's excellent ln 3D vector library. Blanking is used to reposition the laser to a new location, it involves turning off the beam, repositioning and then a pause. The exact pause necessary to clean up an image can vary from projector to projector so this can be easily configured. I am using the methodology outlined in Accurate and Efficient Drawing Method for Laser Projection

If you just want to configure your projector, use examples\parallel_lines\lines.go

go run examples\parallel_lines\lines.go -blank-count=5
# Without sufficient post-blank-count, it produce diagonal lines that cut across most of the image.

Not blanking

go run examples\parallel_lines\lines.go -blank-count=17
# These settings look pretty good on my 30 KPPS projectors. You can still see a small flaw at 17.


// declare some ln Paths
p := ln.Path{ln.Vector{0, 0, 0}, ln.Vector{0, 500, 0}}
p2 := ln.Path{ln.Vector{10000, 0, 0}, ln.Vector{10000, 500, 0}}
// draw speed 0 will use defaults
speed := 0

// in the draw loop
for {
    // draw the first path
    etherdream.DrawPath(w, p, c, speed)
    // use ln Vector.Distance to see if a blank is necessary
    if p2[0].Distance(p[1]) > 0 {
        // blank from p endpoint to p2 startpoint
        etherdream.BlankPath(w, ln.Path{p[1], p2[0]})
    // draw p2
    etherdream.DrawPath(w, p2, c, speed)
    if p2[1].Distance(p[0]) > 0 {
        blank from p2 endbpoint back to original start
        etherdream.BlankPath(w, ln.Path{p2[1], p[0]})


If you are interested in animations, the driver is more precise when you signal the end of a frame in your pointStream. This will flush the buffer and send the frame to the Ether Dream. Currently this is controlled via NextFrame(), but this portion is in active development.

func pointStream(w io.WriteCloser) {
    defer w.Close()
    for {
        // write all the points in a frame
        // count how many, and save the last point

        frameCount := etherdream.NextFrame(w, pointCount, lastPoint)

Using this we can draw a scene. See:

Laser Particles

3D Rendering


ln can also help you with 3D rendering and transformation. You can position 3D primitives within a scene, render those to paths, optimize the order of the paths and then send the result to the projector. See examples\ln1\ln1.go. Aside from the base ln functionality, the one thing to be aware of here is paths.Optimize - without it the ln output creates many unnessesary blank lines.

// render our scene to paths
paths := scene.Render(eye, center, up, width, height, fovy, znear, zfar, step)
// reorder the paths for optimized output

// now we can draw all our paths with the laser

Draw Speed

When a frame takes too long to draw you will see the output flicker. We can adjust the amount of time we take to draw a path to trade precision for frame rate. This gives you a little more control over the perceived quality of your laser output.

go run examples\ln2\ln2.go
# the default draw speed 50 doesn't look very good. Severe flicker.

go run examples\ln2\ln2.go -draw-speed 80
# when I increase the draw speed some distortion appears on the corners, but flicker is almost entirely eliminated.


  • Instead of draw speed, render a frame from vectors according with optimum sample count.
  • Optimization - slow down prior to to sharp angles of movement.
  • Import of SVG/ILDA files.





View Source
const BeginCmd = 0x62

BeginCmd starts playback

View Source
const ColorMax = 65535

ColorMax is the maximum color/intensity value

View Source
const PointSize uint16 = 18

PointSize is the number of bytes in a point struct


View Source
var BlankColor = color.Black

BlankColor is a zeroed out color used for blanking segments

View Source
var BlankCount = flag.Int("blank-count", 20, "How many samples to wait after drawing a blanking line.")

BlankCount is the number of blank samples to insert after moving

View Source
var Debug = flag.Bool("debug", false, "Enable debug output.")

Debug mode

View Source
var DrawSpeed = flag.Float64("draw-speed", 50.0, "Draw speed (25-100). Lower is more precision but slower.")

DrawSpeed affects how many points will be sampled on your lines. Lower is more precise, but is more likely to flicker. Higher values will give smoother playback, but there may be gaps around corners. Try values 25-100.

View Source
var Dump = flag.Bool("dump", false, "Dump point stream to stdout.")

Dump will output the point stream coordinates

View Source
var ScanRate = flag.Int("scan-rate", 24000, "Number of points per second to play back.")

ScanRate controls the playback speed of the ether dream


func DrawPath

func DrawPath(w io.WriteCloser, p ln.Path, c color.Color, drawSpeed float64)

DrawPath will use linear interpolation to draw fn+1 points along the path (fn segments) qual will override the LineQuality (see above).

func FramePoints

func FramePoints() int

FramePoints is the number of points in one frame - 24k / 30 = 800

func NextFrame

func NextFrame(w io.WriteCloser, pointsPlayed int, last Point) int

NextFrame advances playback ... add some blank points

func NumberOfSegments

func NumberOfSegments(p ln.Path, drawSpeed float64) float64

NumberOfSegments to use when interpolating the path

func Osc

func Osc(cur, max int, amplitude, frequency, offset float64) float64

Osc is an oscillator value - send it frame counts / point counts


type BroadcastPacket

type BroadcastPacket struct {
	MAC            []uint8
	HWRev          uint16
	SWRev          uint16
	BufferCapacity uint16
	MaxPointRate   uint32
	Status         *DACStatus

BroadcastPacket is the various capabilities advertised by the DAC

func FindFirstDAC

func FindFirstDAC() (*net.UDPAddr, *BroadcastPacket, error)

FindFirstDAC starts a UDP server to listen for broadcast packets on your network. Return the UDPAddr of the first Ether Dream DAC located

func NewBroadcastPacket

func NewBroadcastPacket(b [36]byte) *BroadcastPacket

NewBroadcastPacket is assembled from 36 bytes of data

func (BroadcastPacket) String

func (bp BroadcastPacket) String() string

type DAC

type DAC struct {
	Host           string
	Port           string
	FirmwareString string
	LastStatus     *DACStatus
	Reader         io.Reader
	Writer         io.WriteCloser
	PointsPlayed   int
	// contains filtered or unexported fields

DAC is the interface to the Ether Dream Digital to Analog Converter that turns network signals into ILDA control singnals for a laser scanner.

func NewDAC

func NewDAC(host string) (*DAC, error)

NewDAC will connect to an Ether Dream device over TCP or it will return an error

func (*DAC) Begin

func (d *DAC) Begin(lwm uint16, rate uint32) (*DACStatus, error)

Begin Playback This causes the DAC to begin producing output. lwm is currently unused. rate is the number of points per second to be read from the buffer. If the playback system was Prepared and there was data in the buffer, then the DAC will reply with ACK; otherwise, it replies with NAK - Invalid.

func (*DAC) ClearEmergencyStop

func (d *DAC) ClearEmergencyStop() (*DACStatus, error)

ClearEmergencyStop command. If the light engine was in E-Stop state due to an emergency stop command (either from a local stop condition or over the network), then this command resets it to be Ready. It is ACKed if the DAC was previously in E-Stop; otherwise it is replied to with a NAK - Invalid. If the condition that caused the emergency stop is still active (E-Stop input still asserted, temperature still out of bounds, etc.), then a NAK - Stop Condition is sent.

func (*DAC) Close

func (d *DAC) Close()

Close the network connection, you should. -- Yoda

func (*DAC) EmergencyStop

func (d *DAC) EmergencyStop() (*DACStatus, error)

EmergencyStop command causes the light engine to enter the E-Stop state, regardless of its previous state. It is always ACKed.

func (*DAC) Measure

func (d *DAC) Measure(stream PointStream)

Measure how long it takes to play 10,000 points

func (*DAC) Ping

func (d *DAC) Ping() (*DACStatus, error)

Ping command

func (*DAC) Play

func (d *DAC) Play(stream PointStream)

Play a stream generator and begin sending output to the laser

func (*DAC) Prepare

func (d *DAC) Prepare() (*DACStatus, error)

Prepare command

func (*DAC) Read

func (d *DAC) Read(l int) ([]byte, error)

func (*DAC) ReadResponse

func (d *DAC) ReadResponse(cmd string) (*DACStatus, error)

ReadResponse reads the ACK/NACK response to a command

func (DAC) Send

func (d DAC) Send(cmd []byte) error

Send a command to the DAC

func (DAC) ShouldPrepare

func (d DAC) ShouldPrepare() bool

ShouldPrepare or not? State 1 and 2 are good. Some Flags need prepare to reset an invalid state.

func (*DAC) Stop

func (d *DAC) Stop() (*DACStatus, error)

Stop command

func (*DAC) Update

func (d *DAC) Update(lwm uint16, rate uint32) (*DACStatus, error)

Update should not exist? Maybe this is the 'q' command now.

func (*DAC) Write

func (d *DAC) Write(b []byte) (*DACStatus, error)

type DACStatus

type DACStatus struct {
	Protocol         uint8
	LightEngineState uint8
	PlaybackState    uint8
	Source           uint8
	LightEngineFlags uint16
	PlaybackFlags    uint16
	SourceFlags      uint16
	BufferFullness   uint16
	PointRate        uint32
	PointCount       uint32

DACStatus is a struct of status informaion sent by the etherdream DAC

func NewDACStatus

func NewDACStatus(b []byte) *DACStatus

func (DACStatus) String

func (st DACStatus) String() string

type Point

type Point struct {
	X     int16
	Y     int16
	R     uint16
	G     uint16
	B     uint16
	I     uint16
	U1    uint16
	U2    uint16
	Flags uint16

Point is a step in the laser stream, X, Y, RGB, Intensity and some other fields.

func BlankPath

func BlankPath(w io.WriteCloser, p ln.Path) *Point

BlankPath will add the necessary pause to effectively blank a path

func NewPoint

func NewPoint(x, y int, c color.Color) *Point

NewPoint wil instantiate a point from the basic attributes.

func (Point) Encode

func (p Point) Encode() []byte

Encode color values into a 18 byte struct Point

Values must be specified for x, y, r, g, and b. If a value is not passed in for the other fields, i will default to max(r, g, b); the rest default to zero.

func (Point) ToVector

func (p Point) ToVector() ln.Vector

type PointStream

type PointStream func(w io.WriteCloser)

PointStream is the interface clients should implement to generate points

type Points

type Points struct {
	Points []Point

Points - Point list

type ProtocolError

type ProtocolError struct {
	Msg string

ProtocolError indicates a protocol level error. I've never seen one, but maybe you will.

func (*ProtocolError) Error

func (e *ProtocolError) Error() string


Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL