concertina

Elecronic concertina
git clone https://git.woozle.org/neale/concertina.git

commit
ddad765
parent
10d8dc5
author
Neale Pickett
date
2026-02-12 12:30:07 -0700 MST
multi-architecture, plus builtin LED abstraction
10 files changed,  +287, -28
M go.mod
A cmd/concertina/board_xiao.go
+17, -0
 1@@ -0,0 +1,17 @@
 2+//go:build xiao
 3+
 4+package main
 5+
 6+import (
 7+	"machine"
 8+	"time"
 9+)
10+
11+func blink() {
12+	machine.LED.High()
13+	time.Sleep(200 * time.Millisecond)
14+	machine.LED.Low()
15+	time.Sleep(200 * time.Millisecond)
16+}
17+	
18+	
M cmd/concertina/concertina.go
+32, -27
 1@@ -1,12 +1,13 @@
 2 package main
 3 
 4 import (
 5+	"context"
 6 	"image/color"
 7 	"machine"
 8 	"time"
 9 	"tinygo.org/x/drivers"
10 	"tinygo.org/x/drivers/ssd1306"
11-	"tinygo.org/x/drivers/ws2812"
12+	"git.woozle.org/neale/concertina/pkg/led"
13 )
14 
15 // For crying out loud.
16@@ -20,35 +21,31 @@ type Concertina struct {
17 	i2c drivers.I2C
18 }
19 
20-func NewConcertina(vs1053Reset machine.Pin) (c *Concertina) {
21-	// Make the LED red during setup
22-	machine.NEOPIXELS_POWER.Configure(machine.PinConfig{machine.PinOutput})
23-	machine.NEOPIXELS_POWER.High()
24-	machine.NEOPIXELS.Configure(machine.PinConfig{machine.PinOutput})
25-	pixel := ws2812.New(machine.NEOPIXELS)
26-	pixel.WriteColors([]color.RGBA{color.RGBA{0x10, 0, 0, 0}})
27-
28-	vs1053Reset.Configure(machine.PinConfig{machine.PinOutput})
29-	vs1053Reset.Low()
30-	machine.DefaultUART.Configure(machine.UARTConfig{
31+func NewConcertina() (*Concertina, error) {
32+	// Initialize tone generator
33+	if err := machine.DefaultUART.Configure(machine.UARTConfig{
34 		BaudRate: 31250,
35-		TX: machine.UART_TX_PIN,
36-		RX: machine.UART_RX_PIN,
37-	})
38+		TX: VS1053_TX,
39+		RX: VS1053_RX,
40+	}); err != nil {
41+		return nil, err
42+	}
43+	VS1053_RESET.Configure(machine.PinConfig{machine.PinOutput})
44+	VS1053_RESET.Low() // Signal a reset
45+
46+	resetTimer := time.After(10 * time.Millisecond)
47 
48 	if err := machine.I2C0.Configure(machine.I2CConfig{}); err != nil {
49-		println(err)
50-		return
51+		return nil, err
52 	}
53 
54-	c = &Concertina {
55+	c := &Concertina {
56 		tonegen: MidiWriter{Writer: machine.DefaultUART},
57 		i2c: machine.I2C0,
58 	}
59 
60-
61-	// ssd1306 setup already has a delay, so we use that to reset the vs1053
62-	vs1053Reset.High()
63+	<-resetTimer // Wait until this timer has elapsed
64+	VS1053_RESET.High()
65 
66 	c.display = ssd1306.NewI2C(c.i2c)
67 	c.display.Configure(
68@@ -62,15 +59,23 @@ func NewConcertina(vs1053Reset machine.Pin) (c *Concertina) {
69 
70 	c.tonegen.SetPatch(22)  // General MIDI harmonica
71 
72-	// init is done, turn off the LED
73-	machine.NEOPIXELS_POWER.Low()
74-
75-	return
76+	return c, nil
77 }
78 
79 func main() {
80-	c := NewConcertina(machine.D3)
81-
82+	// Blink the LED during startup.
83+	blinkCtx, blinkCancel := context.WithCancel(context.Background())
84+	go led.Flash(blinkCtx, led.Builtin, time.Second / 4, time.Second / 4)
85+
86+	c, err := NewConcertina()
87+	if err != nil {
88+		// let the little light blink forever and ever
89+		for {
90+			time.Sleep(1 * time.Hour)
91+		}
92+	}
93+	blinkCancel()
94+	
95 	var x int
96 	for {
97 		c.display.ClearBuffer()
M go.mod
+1, -1
1@@ -1,4 +1,4 @@
2-module woozle.org/neale/concertina
3+module git.woozle.org/neale/concertina
4 
5 go 1.24.4
6 
A pkg/led/board_itsybitsy_m0.go
+12, -0
 1@@ -0,0 +1,12 @@
 2+//go:build itsybitsy_m0
 3+
 4+package led
 5+
 6+import "machine"
 7+import "apa102"
 8+
 9+var Builtin = &DotStar{
10+	apa102.NewSoftwareSPI(machine.PA00, machine.PA01),
11+}
12+
13+
A pkg/led/board_qtpy.go
+8, -0
1@@ -0,0 +1,8 @@
2+//go:build qtpy
3+
4+package led
5+
6+import "machine"
7+
8+var Builtin = NewNeoPixel(machine.NEOPIXELS, machine.NEOPIXELS_POWER)
9+
A pkg/led/board_xiao.go
+23, -0
 1@@ -0,0 +1,23 @@
 2+//go:build xiao
 3+
 4+package led
 5+
 6+import "machine"
 7+
 8+var Builtin = Pin{
 9+	Pin: machine.LED,
10+	pwm: machine.TCC2,	// I think Arduino's variants/XIAO_m0/variant.cpp uses this timer
11+	invert: true,
12+}
13+
14+var RXL = Pin{
15+	Pin: machine.LED_RXL,
16+	pwm: machine.TCC0,
17+	invert: true,
18+}
19+
20+var TXL = Pin{
21+	Pin: machine.LED_TXL,
22+	pwm: machine.TCC0,
23+	invert: true,
24+}
A pkg/led/dotstar.go
+24, -0
 1@@ -0,0 +1,24 @@
 2+package led
 3+
 4+import (
 5+	"image/color"
 6+	"tinygo.org/x/drivers/apa102"
 7+)
 8+
 9+type DotStar struct {
10+	*apa102.Device
11+}
12+
13+func (d *DotStar) Enable() error {
14+	return nil
15+}
16+
17+func (d *DotStar) Disable() {
18+}
19+
20+func (d *DotStar) SetColor(c color.Color) {
21+	// sigh. https://github.com/tinygo-org/drivers/issues/837
22+	rgba := color.RGBAModel.Convert(c).(color.RGBA)
23+	d.WriteColors([]color.RGBA{rgba})
24+}
25+
A pkg/led/led.go
+78, -0
 1@@ -0,0 +1,78 @@
 2+package led
 3+
 4+import "image/color"
 5+import "context"
 6+import "time"
 7+
 8+type LED interface {
 9+	// Enable the LED. This must be called before doing any LED operations.
10+	// This may increase power use by some boards.
11+	Enable() error
12+
13+	// Disable the LED. This may reduce power use by some boards.
14+	Disable()
15+
16+	// Set the LED color as close as possible to c.
17+	// On single-color LEDs (eg. Arduino Uno), this will only change the PWM duty cycle (brightness).
18+	// Setting the color to black will make the LED stop emitting light,
19+	// but if the LED requires a third power line (eg. ws2812), that will still be high.
20+	// Use Disable() to shut off all power to the LED.
21+	SetColor(c color.Color)
22+}
23+
24+// Red enables l and sets is color as close as possible to red, with
25+// brightness b. b=255 is full brightness.
26+//
27+// If your LED only emits one color, "as close as possible" means
28+// whatever color your LED emits :)
29+func Red(l LED, b uint8) {
30+	l.Enable()
31+	l.SetColor(color.RGBA{b, 0, 0, 0})
32+}
33+
34+// On is a shortcut to Red(l, 0xff)
35+func On(l LED) {
36+	Red(l, 0xff)
37+}
38+
39+// Off is a shortcut to Red(l, 0)
40+func Off(l LED) {
41+	Red(l, 0)
42+}
43+
44+// Flash l until ctx is done.
45+//
46+// You can use this to asynchronously flash:
47+//
48+//    ctx, _ := context.WithTimeout(context.Background(), 5 * time.Second)
49+//    blink := 500 * Millisecond
50+//    go Flash(ctx, Builtin, blink, blink)
51+//
52+// Or you could flash forever, possibly to signal an error:
53+//
54+//    blink := 500 * Millisecond
55+//    Flash(context.Background(), Builtin, blink, blink)
56+func Flash(ctx context.Context, l LED, on time.Duration, off time.Duration) {
57+	ledOn := false
58+	var delay time.Duration
59+
60+	for {
61+		ledOn = !ledOn
62+		if ledOn {
63+			delay = on
64+			On(l)
65+		} else {
66+			delay = off
67+			Off(l)
68+		}
69+
70+		// Wait for something to happen
71+		select {
72+		case <-ctx.Done():
73+			Off(l)
74+			return
75+		case <-time.After(delay):
76+			continue
77+		}
78+	}
79+}
A pkg/led/neopixel.go
+37, -0
 1@@ -0,0 +1,37 @@
 2+package led
 3+
 4+import (
 5+	"machine"
 6+	"image/color"
 7+	"tinygo.org/x/drivers/ws2812"
 8+)
 9+
10+type NeoPixel struct {
11+	power machine.Pin
12+	device ws2812.Device
13+}
14+
15+func NewNeoPixel(data, power machine.Pin) *NeoPixel {
16+	return &NeoPixel{
17+		power: power,
18+		device: ws2812.NewWS2812(data),
19+	}
20+}
21+
22+func (p *NeoPixel) Enable() error {
23+	p.device.Pin.Configure(machine.PinConfig{machine.PinOutput})
24+	p.power.Configure(machine.PinConfig{machine.PinOutput})
25+	p.power.High()
26+	return nil
27+}
28+
29+func (p *NeoPixel) Disable() {
30+	p.power.Low()
31+}
32+
33+func (p *NeoPixel) SetColor(c color.Color) {
34+	// sigh. https://github.com/tinygo-org/drivers/issues/837
35+	rgba := color.RGBAModel.Convert(c).(color.RGBA)
36+	p.device.WriteColors([]color.RGBA{rgba})
37+}
38+
A pkg/led/pin.go
+55, -0
 1@@ -0,0 +1,55 @@
 2+//go:build arduino
 3+
 4+package led
 5+
 6+import "machine"
 7+
 8+// BUG: machine should make this an exported interface, or export timerType
 9+type PWM interface {
10+	Configure(config machine.PWMConfig) error
11+	Channel(pin machine.Pin) (channel uint8, err error)
12+	Set(chanel uint8, value uint32)
13+	Top() uint32
14+}
15+
16+type Pin struct {
17+	machine.Pin
18+	pwm PWM
19+	invert bool		// Does a low pin turn the LED on?
20+}
21+
22+func (p Pin) Enable() {
23+	if p.pwm != nil {
24+		pwm.Configure(machine.PWMConfig{})
25+		pwm.Channel(p.Pin)
26+		return
27+	}
28+	p.Configure(machine.PinConfig{machine.PinOutput})
29+}
30+
31+func (p Pin) Disable() {
32+	// XXX: is there a standard way to detach the pwm?
33+}
34+
35+func (p Pin) SetColor(c color.Color) {
36+	// Our LED can only make one color. Convert c to gray so we can calculate intensity.
37+	g := color.RGBAModel.Convert(c).(color.Gray16)
38+	if p.pwm == nil {
39+		state := g.Y > 0 // Power the LED for anything other than black
40+		if (p.invert) {
41+			state = !state
42+		}
43+		p.Pin.Set(state)
44+		return
45+	}		
46+
47+	// Top() is the maximum value you can pass in to set.
48+	// cycle is therefore a fraction of top.
49+	// BUG: https://tinygo.org/docs/tutorials/pwm/ could make this more clear.
50+	// XXX: Is this arithmetic going to overflow uint32?
51+	cycle := pwm.Top() * g.Y / 0xffff
52+	if p.invert {
53+		cycle = pwm.Top() - cycle
54+	}
55+	pwm.Set(cycle)
56+}