diff --git a/.gitignore b/.gitignore
index 8196e5b..658eed1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,13 @@
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
+__debug_bin
# Test binary, built with `go test -c`
*.test
@@ -14,6 +18,9 @@
# Dependency directories (remove the comment below to include it)
# vendor/
+# Go workspace file
+go.work
+
# TinyGo
*.elf
*.uf2
diff --git a/README.md b/README.md
index 2fb6e21..c6515e2 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,37 @@ Ctrl + Shift + P > Tasks: Run Build Task
TODO
-## Why should I use this?
+## Debugging using a browser
+
+**NOTE**: This is a beta feature! Please do not expect it to function properly, if at all.
+
+If your EuroPi project is set up via the `europi.Bootstrap()` execution paradigm, you may operate and visualize it via a browser instead of having to flash it to a EuroPi hardware module or build it via TinyGo.
+
+There are some caveats that must be understood before using this functionality:
+1. This builds via Pure Go and only simulates (to the best of ability) a RP2040 / Pico running the software.
+1. The visualization web page is terrible. While some care was taken to make it minimally usable, it's not pretty and is rife with bugs.
+1. The websocket interface presented can currently only bind to port 8080 on "all" network interfaces.
+1. Projects which do not utilize the `europi.Bootstrap()` execution paradigm will not be able to partake in this functionality.
+1. [pprof](https://github.com/google/pprof/blob/main/README.md) support is enabled while the websocket interface is available, but it does not represent how the code will run on a RP2040 / Pico or within a EuroPi module. It is specifically provided here for debugging multiprocessing functionality and gross abuses of memory and CPU.
+
+### How to get the browser support to work
+
+First, you need [Go installed](https://go.dev/doc/install). Preferably 1.18 or newer.
+
+Second, set up your module project to build with the `europi.Bootstrap()` execution paradigm and add the following line to the `europi.Bootstrap()` parameter list:
+
+```go
+europi.AttachNonPicoWS(true),
+```
+
+Then, execute your module using Go (not TinyGo) and pass a buildflag tag parameter matching [which revision of hardware](hardware/README.md#revision-list) you want to run, like so:
+
+```bash
+go run -tags=revision1 internal/projects/randomskips/randomskips.go
+```
+
+Finally, open up a browser to [localhost:8080](http://localhost:8080/) to see your module in action!
+
+## Why should I use the TinyGo version of the EuroPi firmware?
You probably shouldn't. This is my passion project because I love Go. You should probably be using the official [EuroPi firmware](https://github.com/Allen-Synthesis/EuroPi). But if you are interested in writing a dedicated script for the EuroPi that requires concurrency and faster performance than MicroPython can offer, then maybe this is the right firmware for you!
diff --git a/analog_reader.go b/analog_reader.go
deleted file mode 100644
index d0d4167..0000000
--- a/analog_reader.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package europi
-
-import (
- "machine"
- "math"
-)
-
-const (
- // Calibrated[Min|Max]AI was calculated using the EuroPi calibration program:
- // https://github.com/Allen-Synthesis/EuroPi/blob/main/software/programming_instructions.md#calibrate-the-module
- CalibratedMinAI = 300
- CalibratedMaxAI = 44009
-
- DefaultSamples = 1000
-)
-
-func init() {
- machine.InitADC()
-}
-
-// AnalogReader is an interface for common analog read methods for knobs and cv input.
-type AnalogReader interface {
- Samples(samples uint16)
- ReadVoltage() float32
- Percent() float32
- Range(steps uint16) uint16
-}
-
-// A struct for handling the reading of analogue control voltage.
-// The analogue input allows you to 'read' CV from anywhere between 0 and 12V.
-type AnalogInput struct {
- machine.ADC
- samples uint16
-}
-
-// NewAI creates a new AnalogInput.
-func NewAI(pin machine.Pin) *AnalogInput {
- adc := machine.ADC{Pin: pin}
- adc.Configure(machine.ADCConfig{})
- return &AnalogInput{ADC: adc, samples: DefaultSamples}
-}
-
-// Samples sets the number of reads for an more accurate average read.
-func (a *AnalogInput) Samples(samples uint16) {
- a.samples = samples
-}
-
-// Percent return the percentage of the input's current relative range as a float between 0.0 and 1.0.
-func (a *AnalogInput) Percent() float32 {
- return float32(a.read()) / CalibratedMaxAI
-}
-
-// ReadVoltage return the current read voltage between 0.0 and 10.0 volts.
-func (a *AnalogInput) ReadVoltage() float32 {
- return a.Percent() * MaxVoltage
-}
-
-// Range return a value between 0 and the given steps (not inclusive) based on the range of the analog input.
-func (a *AnalogInput) Range(steps uint16) uint16 {
- return uint16(a.Percent() * float32(steps))
-}
-
-func (a *AnalogInput) read() uint16 {
- var sum int
- for i := 0; i < int(a.samples); i++ {
- sum += Clamp(int(a.Get())-CalibratedMinAI, 0, CalibratedMaxAI)
- }
- return uint16(sum / int(a.samples))
-}
-
-// A struct for handling the reading of knob voltage and position.
-type Knob struct {
- machine.ADC
- samples uint16
-}
-
-// NewKnob creates a new Knob struct.
-func NewKnob(pin machine.Pin) *Knob {
- adc := machine.ADC{Pin: pin}
- adc.Configure(machine.ADCConfig{})
- return &Knob{ADC: adc, samples: DefaultSamples}
-}
-
-// Samples sets the number of reads for an more accurate average read.
-func (k *Knob) Samples(samples uint16) {
- k.samples = samples
-}
-
-// Percent return the percentage of the knob's current relative range as a float between 0.0 and 1.0.
-func (k *Knob) Percent() float32 {
- return 1 - float32(k.read())/math.MaxUint16
-}
-
-// ReadVoltage return the current read voltage between 0.0 and 10.0 volts.
-func (k *Knob) ReadVoltage() float32 {
- return k.Percent() * MaxVoltage
-}
-
-// Range return a value between 0 and the given steps (not inclusive) based on the range of the knob's position.
-func (k *Knob) Range(steps uint16) uint16 {
- return uint16(k.Percent() * float32(steps))
-}
-
-func (k *Knob) read() uint16 {
- var sum int
- for i := 0; i < int(k.samples); i++ {
- sum += int(k.Get())
- }
- return uint16(sum / int(k.samples))
-}
diff --git a/clamp/clamp.go b/clamp/clamp.go
new file mode 100644
index 0000000..dab53a6
--- /dev/null
+++ b/clamp/clamp.go
@@ -0,0 +1,15 @@
+package clamp
+
+type Clampable interface {
+ ~int8 | ~int16 | ~int32 | ~int64 | ~int | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64
+}
+
+func Clamp[T Clampable](v, min, max T) T {
+ if v > max {
+ v = max
+ }
+ if v < min {
+ v = min
+ }
+ return v
+}
diff --git a/clamp/clamp_test.go b/clamp/clamp_test.go
new file mode 100644
index 0000000..1e4d9a5
--- /dev/null
+++ b/clamp/clamp_test.go
@@ -0,0 +1,41 @@
+package clamp_test
+
+import (
+ "testing"
+
+ "github.com/awonak/EuroPiGo/clamp"
+)
+
+func TestClamp(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ if expected, actual := min, clamp.Clamp(min, min, max); actual != expected {
+ t.Fatalf("Clamp[%v, %v] Clamp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ if expected, actual := max, clamp.Clamp(max, min, max); actual != expected {
+ t.Fatalf("Clamp[%v, %v] Clamp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ if expected, actual := min, clamp.Clamp(min-2, min, max); actual != expected {
+ t.Fatalf("Clamp[%v, %v] Clamp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ if expected, actual := max, clamp.Clamp(max+2, min, max); actual != expected {
+ t.Fatalf("Clamp[%v, %v] Clamp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+}
diff --git a/debounce/debounce.go b/debounce/debounce.go
new file mode 100644
index 0000000..46b74bf
--- /dev/null
+++ b/debounce/debounce.go
@@ -0,0 +1,34 @@
+package debounce
+
+import "time"
+
+type Debouncer[T func(U, time.Duration), U any] interface {
+ Debounce(delay time.Duration) func(U)
+ LastChange() time.Time
+}
+
+type debouncer[T func(U, time.Duration), U any] struct {
+ last time.Time
+ fn T
+}
+
+func NewDebouncer[T func(U, time.Duration), U any](fn T) Debouncer[T, U] {
+ return &debouncer[T, U]{
+ last: time.Now(),
+ fn: fn,
+ }
+}
+
+func (d *debouncer[T, U]) Debounce(delay time.Duration) func(U) {
+ return func(u U) {
+ now := time.Now()
+ if deltaTime := now.Sub(d.last); deltaTime >= delay {
+ d.last = now
+ d.fn(u, deltaTime)
+ }
+ }
+}
+
+func (d debouncer[T, U]) LastChange() time.Time {
+ return d.last
+}
diff --git a/debounce/debounce_test.go b/debounce/debounce_test.go
new file mode 100644
index 0000000..38311ae
--- /dev/null
+++ b/debounce/debounce_test.go
@@ -0,0 +1,89 @@
+package debounce_test
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/awonak/EuroPiGo/debounce"
+)
+
+func TestDebouncer(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
+ fn := func(any, time.Duration) {}
+ if actual := debounce.NewDebouncer(fn); actual == nil {
+ t.Fatalf("Debouncer[%p] NewDebouncer: expected[non-nil] actual[nil]", fn)
+ }
+ })
+
+ t.Run("Debounce", func(t *testing.T) {
+ t.Run("Delay0", func(t *testing.T) {
+ t.Parallel()
+ delay := time.Duration(0)
+ runs := 4
+ runDebouncerTest(t, runs, delay, time.Duration(0), 4)
+ runDebouncerTest(t, runs, delay, time.Millisecond*1, 4)
+ runDebouncerTest(t, runs, delay, time.Second*1, 4)
+ })
+
+ t.Run("Delay100ms", func(t *testing.T) {
+ t.Parallel()
+ delay := time.Millisecond * 100
+ runs := 4
+ runDebouncerTest(t, runs, delay, time.Duration(0), 0)
+ runDebouncerTest(t, runs, delay, time.Millisecond*1, 0)
+ runDebouncerTest(t, runs, delay, time.Second*1, 2)
+ })
+
+ t.Run("Delay10s", func(t *testing.T) {
+ t.Parallel()
+ delay := time.Millisecond * 100
+ runs := 4
+ runDebouncerTest(t, runs, delay, time.Duration(0), 0)
+ runDebouncerTest(t, runs, delay, time.Millisecond*1, 0)
+ runDebouncerTest(t, runs, delay, time.Second*1, 0)
+ })
+ })
+
+ t.Run("LastChange", func(t *testing.T) {
+ fn := func(any, time.Duration) {}
+ db := debounce.NewDebouncer(fn)
+ dbFunc := db.Debounce(0)
+
+ beforeExpected := time.Now()
+ dbFunc(nil)
+ if actual := db.LastChange(); !actual.After(beforeExpected) {
+ t.Fatalf("Debouncer[%p] LastChange: expected(after)[%v] actual[%v]", fn, beforeExpected, actual)
+ }
+ })
+}
+
+func runDebouncerTest(t *testing.T, runs int, delay, interval time.Duration, minimumExpected int) {
+ t.Helper()
+
+ var testName string
+ if interval == 0 {
+ testName = "Immediate"
+ } else {
+ testName = fmt.Sprintf("Interval%v", interval)
+ }
+
+ var actual int
+ fn := func(any, time.Duration) {
+ actual++
+
+ }
+
+ db := debounce.NewDebouncer(fn).Debounce(delay)
+
+ t.Run(testName, func(t *testing.T) {
+ for i := 0; i < runs; i++ {
+ db(nil)
+ time.Sleep(interval)
+ }
+ // since these are timing-based, we have to be lenient
+ if actual < minimumExpected {
+ t.Fatalf("Debouncer[%p] Debounce(%v): expected[%v] actual[%v]", fn, delay, minimumExpected, actual)
+ }
+ })
+}
diff --git a/helpers.go b/debug.go
similarity index 65%
rename from helpers.go
rename to debug.go
index 832f92a..18294e5 100644
--- a/helpers.go
+++ b/debug.go
@@ -1,22 +1,12 @@
package europi
import (
+ "context"
"log"
"runtime"
"time"
)
-// Clamp returns a value that is no lower than "low" and no higher than "high".
-func Clamp[V uint8 | uint16 | int | float32](value, low, high V) V {
- if value >= high {
- return high
- }
- if value <= low {
- return low
- }
- return value
-}
-
func DebugMemoryUsage() {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
@@ -40,3 +30,19 @@ func DebugMemoryUsedPerSecond() {
time.Sleep(time.Second)
}
}
+
+// used for non-pico testing of europi apps
+var (
+ activateNonPicoWebSocket func(ctx context.Context, e Hardware) NonPicoWSActivation
+)
+
+type NonPicoWSActivation interface {
+ Shutdown() error
+}
+
+func ActivateNonPicoWS(ctx context.Context, e Hardware) NonPicoWSActivation {
+ if activateNonPicoWebSocket == nil {
+ return nil
+ }
+ return activateNonPicoWebSocket(ctx, e)
+}
diff --git a/debug_nonpico.go b/debug_nonpico.go
new file mode 100644
index 0000000..d8baccf
--- /dev/null
+++ b/debug_nonpico.go
@@ -0,0 +1,19 @@
+//go:build !pico
+// +build !pico
+
+package europi
+
+import (
+ "context"
+
+ "github.com/awonak/EuroPiGo/internal/nonpico"
+)
+
+func nonPicoActivateWebSocket(ctx context.Context, e Hardware) NonPicoWSActivation {
+ nonPicoWSApi := nonpico.ActivateWebSocket(ctx, e.Revision())
+ return nonPicoWSApi
+}
+
+func init() {
+ activateNonPicoWebSocket = nonPicoActivateWebSocket
+}
diff --git a/digital_reader.go b/digital_reader.go
deleted file mode 100644
index eb8bbe5..0000000
--- a/digital_reader.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package europi
-
-import (
- "machine"
- "time"
-)
-
-const DefaultDebounceDelay = time.Duration(50 * time.Millisecond)
-
-// DigitalReader is an interface for common digital inputs methods.
-type DigitalReader interface {
- Handler(func(machine.Pin))
- HandlerWithDebounce(func(machine.Pin), time.Duration)
- LastInput() time.Time
- Value() bool
-}
-
-// DigitalInput is a struct for handling reading of the digital input.
-type DigitalInput struct {
- Pin machine.Pin
- debounceDelay time.Duration
- lastInput time.Time
- callback func(p machine.Pin)
-}
-
-// NewDI creates a new DigitalInput struct.
-func NewDI(pin machine.Pin) *DigitalInput {
- pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
- return &DigitalInput{
- Pin: pin,
- lastInput: time.Now(),
- debounceDelay: DefaultDebounceDelay,
- }
-}
-
-// LastInput return the time of the last high input (triggered at 0.8v).
-func (d *DigitalInput) LastInput() time.Time {
- return d.lastInput
-}
-
-// Value returns true if the input is high (above 0.8v), else false.
-func (d *DigitalInput) Value() bool {
- // Invert signal to match expected behavior.
- return !d.Pin.Get()
-}
-
-// Handler sets the callback function to be call when a rising edge is detected.
-func (d *DigitalInput) Handler(handler func(p machine.Pin)) {
- d.HandlerWithDebounce(handler, 0)
-}
-
-// Handler sets the callback function to be call when a rising edge is detected and debounce delay time has elapsed.
-func (d *DigitalInput) HandlerWithDebounce(handler func(p machine.Pin), delay time.Duration) {
- d.callback = handler
- d.debounceDelay = delay
- d.Pin.SetInterrupt(machine.PinFalling, d.debounceWrapper)
-}
-
-func (d *DigitalInput) debounceWrapper(p machine.Pin) {
- t := time.Now()
- if t.Before(d.lastInput.Add(d.debounceDelay)) {
- return
- }
- d.callback(p)
- d.lastInput = t
-}
-
-// Button is a struct for handling push button behavior.
-type Button struct {
- Pin machine.Pin
- debounceDelay time.Duration
- lastInput time.Time
- callback func(p machine.Pin)
-}
-
-// NewButton creates a new Button struct.
-func NewButton(pin machine.Pin) *Button {
- pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
- return &Button{
- Pin: pin,
- lastInput: time.Now(),
- debounceDelay: DefaultDebounceDelay,
- }
-}
-
-// Handler sets the callback function to be call when the button is pressed.
-func (b *Button) Handler(handler func(p machine.Pin)) {
- b.HandlerWithDebounce(handler, 0)
-}
-
-// Handler sets the callback function to be call when the button is pressed and debounce delay time has elapsed.
-func (b *Button) HandlerWithDebounce(handler func(p machine.Pin), delay time.Duration) {
- b.callback = handler
- b.debounceDelay = delay
- b.Pin.SetInterrupt(machine.PinFalling, b.debounceWrapper)
-}
-
-func (b *Button) debounceWrapper(p machine.Pin) {
- t := time.Now()
- if t.Before(b.lastInput.Add(b.debounceDelay)) {
- return
- }
- b.callback(p)
- b.lastInput = t
-}
-
-// LastInput return the time of the last button press.
-func (b *Button) LastInput() time.Time {
- return b.lastInput
-}
-
-// Value returns true if button is currently pressed, else false.
-func (b *Button) Value() bool {
- // Invert signal to match expected behavior.
- return !b.Pin.Get()
-}
diff --git a/display.go b/display.go
deleted file mode 100644
index 15fbad8..0000000
--- a/display.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package europi
-
-import (
- "image/color"
- "machine"
-
- "tinygo.org/x/drivers/ssd1306"
- "tinygo.org/x/tinyfont"
- "tinygo.org/x/tinyfont/proggy"
-)
-
-const (
- OLEDFreq = machine.TWI_FREQ_400KHZ
- OLEDAddr = 0x3C
- OLEDWidth = 128
- OLEDHeight = 32
-)
-
-var (
- DefaultChannel = machine.I2C0
- DefaultFont = &proggy.TinySZ8pt7b
- White = color.RGBA{255, 255, 255, 255}
-)
-
-// Display is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED.
-type Display struct {
- ssd1306.Device
- font *tinyfont.Font
-}
-
-// NewDisplay returns a new Display struct.
-func NewDisplay(channel *machine.I2C, sdaPin, sclPin machine.Pin) *Display {
- channel.Configure(machine.I2CConfig{
- Frequency: OLEDFreq,
- SDA: sdaPin,
- SCL: sclPin,
- })
-
- display := ssd1306.NewI2C(DefaultChannel)
- display.Configure(ssd1306.Config{
- Address: OLEDAddr,
- Width: OLEDWidth,
- Height: OLEDHeight,
- })
- return &Display{Device: display, font: DefaultFont}
-}
-
-// Font overrides the default font used by `WriteLine`.
-func (d *Display) Font(font *tinyfont.Font) {
- d.font = font
-}
-
-// WriteLine writes the given text to the display where x, y is the bottom leftmost pixel of the text.
-func (d *Display) WriteLine(text string, x, y int16) {
- tinyfont.WriteLine(d, d.font, x, y, text, White)
-}
diff --git a/europi.go b/europi.go
index db5a15e..cfadb84 100644
--- a/europi.go
+++ b/europi.go
@@ -1,64 +1,115 @@
-package europi // import europi "github.com/awonak/EuroPiGo"
+package europi // import "github.com/awonak/EuroPiGo"
import (
- "machine"
+ "github.com/awonak/EuroPiGo/hardware"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/hardware/rev0"
+ "github.com/awonak/EuroPiGo/hardware/rev1"
+ _ "github.com/awonak/EuroPiGo/internal/nonpico"
+ _ "github.com/awonak/EuroPiGo/internal/pico"
)
-const (
- MaxVoltage = 10.0
- MinVoltage = 0.0
+type (
+ // Hardware is the collection of component wrappers used to interact with the module.
+ Hardware = hal.Hardware
+
+ // EuroPiPrototype is the revision 0 hardware
+ EuroPiPrototype = rev0.EuroPiPrototype
+ // EuroPi is the revision 1 hardware
+ EuroPi = rev1.EuroPi
+ // TODO: add rev2
)
-// EuroPi is the collection of component wrappers used to interact with the module.
-type EuroPi struct {
- // Display is a wrapper around ssd1306.Device
- Display *Display
+// New will return a new EuroPi struct based on the detected hardware revision
+func New() Hardware {
+ // ensure our hardware has been identified
+ ensureHardware()
+
+ // blocks until revision has been identified
+ revision := hardware.GetRevision()
+ return NewFrom(revision)
+}
+
+// NewFrom will return a new EuroPi struct based on a specific revision
+func NewFrom(revision hal.Revision) Hardware {
+ if revision == hal.RevisionUnknown {
+ // unknown revision
+ return nil
+ }
+
+ // ensure our hardware has been identified
+ ensureHardware()
+
+ // this will block until the hardware components are initialized
+ hardware.WaitForReady()
+
+ switch revision {
+ case hal.Revision0:
+ return rev0.Pi
+ case hal.Revision1:
+ return rev1.Pi
+ case hal.Revision2:
+ // TODO: add rev2
+ return nil
+ default:
+ return nil
+ }
+}
- DI DigitalReader
- AI AnalogReader
+// Display returns the primary display from the hardware interface, if it has one
+func Display(e Hardware) hal.DisplayOutput {
+ if e == nil {
+ return nil
+ }
- B1 DigitalReader
- B2 DigitalReader
+ switch e.Revision() {
+ case hal.Revision1:
+ return e.(*rev1.EuroPi).OLED
+ case hal.Revision2:
+ // TODO: add rev2
+ //return e.(*rev2.EuroPiX).Display
+ }
+ return nil
+}
- K1 AnalogReader
- K2 AnalogReader
+// Button returns a button input at the specified index from the hardware interface,
+// if it has one there
+func Button(e Hardware, idx int) hal.ButtonInput {
+ if e == nil {
+ return nil
+ }
- CV1 Outputer
- CV2 Outputer
- CV3 Outputer
- CV4 Outputer
- CV5 Outputer
- CV6 Outputer
- CV [6]Outputer
+ switch e.Revision() {
+ case hal.Revision0:
+ return e.(*rev0.EuroPiPrototype).Button(idx)
+ case hal.Revision1:
+ return e.(*rev1.EuroPi).Button(idx)
+ case hal.Revision2:
+ // TODO: add rev2
+ //return e.(*rev2.EuroPiX).Button(idx)
+ }
+ return nil
}
-// New will return a new EuroPi struct.
-func New() *EuroPi {
- cv1 := NewOutput(machine.GPIO21, machine.PWM2)
- cv2 := NewOutput(machine.GPIO20, machine.PWM2)
- cv3 := NewOutput(machine.GPIO16, machine.PWM0)
- cv4 := NewOutput(machine.GPIO17, machine.PWM0)
- cv5 := NewOutput(machine.GPIO18, machine.PWM1)
- cv6 := NewOutput(machine.GPIO19, machine.PWM1)
-
- return &EuroPi{
- Display: NewDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1),
-
- DI: NewDI(machine.GPIO22),
- AI: NewAI(machine.ADC0),
-
- B1: NewButton(machine.GPIO4),
- B2: NewButton(machine.GPIO5),
-
- K1: NewKnob(machine.ADC1),
- K2: NewKnob(machine.ADC2),
-
- CV1: cv1,
- CV2: cv2,
- CV3: cv3,
- CV4: cv4,
- CV5: cv5,
- CV6: cv5,
- CV: [6]Outputer{cv1, cv2, cv3, cv4, cv5, cv6},
+// Knob returns a knob input at the specified index from the hardware interface,
+// if it has one there
+func Knob(e Hardware, idx int) hal.KnobInput {
+ if e == nil {
+ return nil
+ }
+
+ switch e.Revision() {
+ case hal.Revision0:
+ return e.(*rev0.EuroPiPrototype).Knob(idx)
+ case hal.Revision1:
+ return e.(*rev1.EuroPi).Knob(idx)
+ case hal.Revision2:
+ // TODO: add rev2
+ //return e.(*rev2.EuroPiX).Knob(idx)
}
+ return nil
}
+
+// ensureHardware is part of the hardware setup system.
+// It will be set to a function that can properly configure the hardware for use.
+var ensureHardware func()
diff --git a/europi_nonpico.go b/europi_nonpico.go
new file mode 100644
index 0000000..ad3ca2e
--- /dev/null
+++ b/europi_nonpico.go
@@ -0,0 +1,24 @@
+//go:build !pico
+// +build !pico
+
+package europi
+
+import (
+ "sync"
+
+ "github.com/awonak/EuroPiGo/hardware"
+ "github.com/awonak/EuroPiGo/internal/nonpico"
+)
+
+var nonPicoEnsureHardwareOnce sync.Once
+
+func nonPicoEnsureHardware() {
+ nonPicoEnsureHardwareOnce.Do(func() {
+ rev := nonpico.DetectRevision()
+ hardware.SetDetectedRevision(rev)
+ })
+}
+
+func init() {
+ ensureHardware = nonPicoEnsureHardware
+}
diff --git a/europi_pico.go b/europi_pico.go
new file mode 100644
index 0000000..5076242
--- /dev/null
+++ b/europi_pico.go
@@ -0,0 +1,24 @@
+//go:build pico
+// +build pico
+
+package europi
+
+import (
+ "sync"
+
+ "github.com/awonak/EuroPiGo/hardware"
+ "github.com/awonak/EuroPiGo/internal/pico"
+)
+
+var picoEnsureHardwareOnce sync.Once
+
+func picoEnsureHardware() {
+ picoEnsureHardwareOnce.Do(func() {
+ rev := pico.DetectRevision()
+ hardware.SetDetectedRevision(rev)
+ })
+}
+
+func init() {
+ ensureHardware = picoEnsureHardware
+}
diff --git a/europi_test.go b/europi_test.go
new file mode 100644
index 0000000..09685e7
--- /dev/null
+++ b/europi_test.go
@@ -0,0 +1,67 @@
+package europi_test
+
+import (
+ "testing"
+
+ europi "github.com/awonak/EuroPiGo"
+ "github.com/awonak/EuroPiGo/hardware"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/hardware/rev0"
+ "github.com/awonak/EuroPiGo/hardware/rev1"
+)
+
+func TestNew(t *testing.T) {
+ t.Run("Default", func(t *testing.T) {
+ // unless we're running on a pico, we should expect a nil back for the default revision
+ hardware.SetDetectedRevision(hal.RevisionUnknown)
+ if actual := europi.New(); actual != nil {
+ t.Fatalf("EuroPi New: expected[nil] actual[%T]", actual)
+ }
+ })
+
+ t.Run("Revision0", func(t *testing.T) {
+ hardware.SetDetectedRevision(hal.Revision0)
+ pi := europi.New()
+ switch actual := pi.(type) {
+ case *rev0.EuroPiPrototype:
+ if actual == nil {
+ t.Fatal("EuroPi New: expected[non-nil] actual[nil]")
+ }
+ case nil:
+ t.Fatal("EuroPi New: expected[non-nil] actual[nil]")
+ default:
+ t.Fatalf("EuroPi New: expected[EuroPi Prototype] actual[%v]", actual)
+ }
+ })
+
+ t.Run("Revision1", func(t *testing.T) {
+ hardware.SetDetectedRevision(hal.Revision1)
+ pi := europi.New()
+ switch actual := pi.(type) {
+ case *rev1.EuroPi:
+ if actual == nil {
+ t.Fatal("EuroPi New: expected[non-nil] actual[nil]")
+ }
+ case nil:
+ t.Fatal("EuroPi New: expected[non-nil] actual[nil]")
+ default:
+ t.Fatalf("EuroPi New: expected[EuroPi] actual[%v]", actual)
+ }
+ })
+}
+
+func TestNewFrom(t *testing.T) {
+ t.Run("Revision0", func(t *testing.T) {
+ hardware.SetDetectedRevision(hal.Revision0)
+ if actual, _ := europi.NewFrom(hal.Revision0).(*rev0.EuroPiPrototype); actual == nil {
+ t.Fatalf("EuroPi NewFrom: expected[EuroPiPrototype] actual[%T]", actual)
+ }
+ })
+
+ t.Run("Revision1", func(t *testing.T) {
+ hardware.SetDetectedRevision(hal.Revision1)
+ if actual, _ := europi.NewFrom(hal.Revision1).(*rev1.EuroPi); actual == nil {
+ t.Fatalf("EuroPi NewFrom: expected[EuroPi] actual[%T]", actual)
+ }
+ })
+}
diff --git a/event/README.md b/event/README.md
new file mode 100644
index 0000000..d708d49
--- /dev/null
+++ b/event/README.md
@@ -0,0 +1,15 @@
+# Event Bus
+
+A multi-provider, single-subscriber callback/events dispatcher.
+
+## What's It For?
+
+Under most circumstances, you wouldn't want or need this. It's not optimal - nor does it intend to be such - though it *is* thread-safe.
+
+## Why Not Use Channels?
+
+Channels are nice and all, but they're a bit heavy-weight for what this was intended to be used for. They also require more setup and management. For a full-featured and robust solution, a system based on them makes a lot more sense. This did not intend to be full-featured, nor robust.
+
+## What's This Package Good For, Then?
+
+Some testing apparati need a way to dictate asynchronous operations of simulated hardware. This just happened to be a simple solution for that.
diff --git a/event/bus.go b/event/bus.go
new file mode 100644
index 0000000..4ce7f51
--- /dev/null
+++ b/event/bus.go
@@ -0,0 +1,45 @@
+package event
+
+import "sync"
+
+type Bus interface {
+ Subscribe(subject string, callback func(msg any))
+ Unsubscribe(subject string)
+ Post(subject string, msg any)
+}
+
+func Subscribe[TMsg any](bus Bus, subject string, callback func(msg TMsg)) {
+ bus.Subscribe(subject, func(msg any) {
+ if tmsg, ok := msg.(TMsg); ok {
+ callback(tmsg)
+ }
+ })
+}
+
+func NewBus() Bus {
+ b := &bus{}
+ return b
+}
+
+type bus struct {
+ chMap sync.Map
+}
+
+func (b *bus) Subscribe(subject string, callback func(msg any)) {
+ b.chMap.Store(subject, callback)
+}
+
+func (b *bus) Unsubscribe(subject string) {
+ b.chMap.Delete(subject)
+}
+
+func (b *bus) Post(subject string, msg any) {
+ cb, found := b.chMap.Load(subject)
+ if !found {
+ return
+ }
+
+ if callback, ok := cb.(func(any)); ok {
+ callback(msg)
+ }
+}
diff --git a/event/bus_test.go b/event/bus_test.go
new file mode 100644
index 0000000..31a2219
--- /dev/null
+++ b/event/bus_test.go
@@ -0,0 +1,90 @@
+package event_test
+
+import (
+ "fmt"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/awonak/EuroPiGo/event"
+)
+
+func TestBus(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
+ if actual := event.NewBus(); actual == nil {
+ t.Fatal("Bus NewBus: expected[non-nil] actual[nil]")
+ }
+ })
+
+ t.Run("Post", func(t *testing.T) {
+ bus := event.NewBus()
+
+ t.Run("Unsubscribed", func(t *testing.T) {
+ // test to see if we block on a post to an unsubscribed subject
+ subject := fmt.Sprintf("testing_%v", time.Now().UnixNano())
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ bus.Post(subject, "hello world")
+ }()
+
+ // if we somehow block here, then the test deadline will eventually be reached
+ // and then it will be failed automatically
+ wg.Wait()
+ })
+ })
+
+ t.Run("Subscribe", func(t *testing.T) {
+ bus := event.NewBus()
+ t.Run("Untyped", func(t *testing.T) {
+ var actual any
+ subject := "untyped"
+ bus.Subscribe(subject, func(msg any) {
+ actual = msg
+ })
+
+ expected := 5
+ bus.Post(subject, expected)
+ if actual != expected {
+ t.Fatalf("Bus Subscribe(%v): expected[%v] actual[%v]", subject, expected, actual)
+ }
+ })
+
+ t.Run("Typed", func(t *testing.T) {
+ var actual string
+ subject := "typed"
+ event.Subscribe(bus, subject, func(msg string) {
+ actual = msg
+ })
+
+ expected := "hello world"
+ bus.Post(subject, expected)
+ if actual != expected {
+ t.Fatalf("Bus Subscribe(%v): expected[%v] actual[%v]", subject, expected, actual)
+ }
+ })
+ })
+
+ t.Run("Unsubscribe", func(t *testing.T) {
+ bus := event.NewBus()
+
+ t.Run("Subscribed", func(t *testing.T) {
+ subject := "unsub"
+
+ var actual any
+ bus.Subscribe(subject, func(msg any) {
+ actual = msg
+ })
+
+ bus.Unsubscribe(subject)
+
+ var expected any
+ bus.Post(subject, "hello world")
+ if actual != expected {
+ t.Fatalf("Bus Unsubscribe(%v): expected[%v] actual[%v]", subject, expected, actual)
+ }
+ })
+ })
+}
diff --git a/experimental/displaylogger/logger.go b/experimental/displaylogger/logger.go
new file mode 100644
index 0000000..597a59a
--- /dev/null
+++ b/experimental/displaylogger/logger.go
@@ -0,0 +1,74 @@
+package displaylogger
+
+import (
+ "io"
+ "strings"
+
+ "github.com/awonak/EuroPiGo/experimental/draw"
+ "github.com/awonak/EuroPiGo/experimental/fontwriter"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "tinygo.org/x/tinyfont/proggy"
+)
+
+var (
+ DefaultFont = &proggy.TinySZ8pt7b
+)
+
+type Logger interface {
+ io.Writer
+ Flush()
+}
+
+type logger struct {
+ sb strings.Builder
+ display hal.DisplayOutput
+ writer fontwriter.Writer
+}
+
+func NewLogger(display hal.DisplayOutput) Logger {
+ return &logger{
+ sb: strings.Builder{},
+ display: display,
+ writer: fontwriter.Writer{
+ Display: display,
+ Font: DefaultFont,
+ },
+ }
+}
+
+func (w *logger) Write(p []byte) (n int, err error) {
+ n, err = w.sb.Write(p)
+ if err != nil {
+ return
+ }
+
+ w.repaint()
+ return
+}
+
+func (w *logger) Flush() {
+ w.repaint()
+}
+
+func (w *logger) repaint() {
+ str := w.sb.String()
+
+ w.display.ClearBuffer()
+
+ lines := strings.Split(str, "\n")
+ w.sb.Reset()
+ _, maxY := w.display.Size()
+ yAdv := w.writer.Font.GetYAdvance()
+ maxLines := (maxY + int16(yAdv) - 1) / int16(yAdv)
+ for l := len(lines); l > int(maxLines); l-- {
+ lines = lines[1:]
+ }
+ w.sb.WriteString(strings.Join(lines, "\n"))
+
+ liney := yAdv
+ for _, s := range lines {
+ w.writer.WriteLine(s, 0, int16(liney), draw.White)
+ liney += yAdv
+ }
+ _ = w.display.Display()
+}
diff --git a/experimental/draw/colors.go b/experimental/draw/colors.go
new file mode 100644
index 0000000..e14e13a
--- /dev/null
+++ b/experimental/draw/colors.go
@@ -0,0 +1,8 @@
+package draw
+
+import "image/color"
+
+var (
+ White = color.RGBA{255, 255, 255, 255}
+ Black = color.RGBA{0, 0, 0, 255}
+)
diff --git a/experimental/fontwriter/alignment.go b/experimental/fontwriter/alignment.go
new file mode 100644
index 0000000..b3ff377
--- /dev/null
+++ b/experimental/fontwriter/alignment.go
@@ -0,0 +1,17 @@
+package fontwriter
+
+type HorizontalAlignment int
+
+const (
+ AlignLeft = HorizontalAlignment(iota)
+ AlignCenter
+ AlignRight
+)
+
+type VerticalAlignment int
+
+const (
+ AlignTop = VerticalAlignment(iota)
+ AlignMiddle
+ AlignBottom
+)
diff --git a/experimental/fontwriter/writer.go b/experimental/fontwriter/writer.go
new file mode 100644
index 0000000..77eaf0d
--- /dev/null
+++ b/experimental/fontwriter/writer.go
@@ -0,0 +1,112 @@
+package fontwriter
+
+import (
+ "image/color"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "tinygo.org/x/tinydraw"
+ "tinygo.org/x/tinyfont"
+)
+
+type Writer struct {
+ Display hal.DisplayOutput
+ Font tinyfont.Fonter
+}
+
+// WriteLine writes the given text to the display where x, y is the bottom leftmost pixel of the text.
+func (w *Writer) WriteLine(s string, x, y int16, c color.RGBA) {
+ tinyfont.WriteLine(w.Display, w.Font, x, y, s, c)
+}
+
+// WriteLineAligned writes the given text to the display where:
+// x, y is the bottom leftmost pixel of the text
+// alignh is horizontal alignment
+// alignv is vertical alignment
+func (w *Writer) WriteLineAligned(text string, x, y int16, c color.RGBA, alignh HorizontalAlignment, alignv VerticalAlignment) {
+ w.writeLineAligned(text, x, y, c, alignh, alignv)
+}
+
+func (w *Writer) writeLineAligned(text string, x, y int16, c color.RGBA, alignh HorizontalAlignment, alignv VerticalAlignment) {
+ x0, y0 := x, y
+ switch alignh {
+ case AlignLeft:
+ case AlignCenter:
+ dispWidth, _ := w.Display.Size()
+ _, outerWidth := tinyfont.LineWidth(w.Font, text)
+ x0 = (dispWidth-int16(outerWidth))/2 - x
+ case AlignRight:
+ dispWidth, _ := w.Display.Size()
+ _, outerWidth := tinyfont.LineWidth(w.Font, text)
+ x0 = dispWidth - int16(outerWidth) - x
+ default:
+ panic("invalid alignment")
+ }
+ tinyfont.WriteLine(w.Display, w.Font, x0, y0, text, c)
+}
+
+// WriteLineInverse writes the given text to the display in an inverted way where x, y is the bottom leftmost pixel of the text
+func (w *Writer) WriteLineInverse(text string, x, y int16, c color.RGBA) {
+ inverseC := color.RGBA{
+ R: ^c.R,
+ G: ^c.G,
+ B: ^c.B,
+ A: c.A,
+ }
+ _, outerWidth := tinyfont.LineWidth(w.Font, text)
+ outerHeight := int16(w.Font.GetYAdvance())
+ x0, y0 := x, y-outerHeight+2
+ x1, y1 := x+1, y
+ _ = tinydraw.FilledRectangle(w.Display, x0, y0, int16(outerWidth+2), outerHeight, c)
+ tinyfont.WriteLine(w.Display, w.Font, x1, y1, text, inverseC)
+}
+
+// WriteLineInverseAligned writes the given text to the display in an inverted way where:
+// x, y is the bottom leftmost pixel of the text
+// alignh is horizontal alignment
+// alignv is vertical alignment
+func (w *Writer) WriteLineInverseAligned(text string, x, y int16, c color.RGBA, alignh HorizontalAlignment, alignv VerticalAlignment) {
+ w.writeLineInverseAligned(text, w.Font, x, y, c, alignh, alignv)
+}
+
+func (w *Writer) writeLineInverseAligned(text string, font tinyfont.Fonter, x, y int16, c color.RGBA, alignh HorizontalAlignment, alignv VerticalAlignment) {
+ _, outerWidth := tinyfont.LineWidth(font, text)
+ outerHeight := int16(font.GetYAdvance())
+ x0, y0 := x, y-outerHeight+2
+ x1, y1 := x+1, y
+ switch alignh {
+ case AlignLeft:
+ case AlignCenter:
+ dispWidth, _ := w.Display.Size()
+ x0 = (dispWidth-int16(outerWidth))/2 - x
+ x1 = x0 + 1
+ case AlignRight:
+ dispWidth, _ := w.Display.Size()
+ x0 = dispWidth - int16(outerWidth) - x
+ x1 = x0 + 1
+ default:
+ panic("invalid alignment")
+ }
+ switch alignv {
+ case AlignTop:
+ case AlignMiddle:
+ _, dispHeight := w.Display.Size()
+ midY := (dispHeight - outerHeight) / 2
+ y0 += midY
+ y1 += midY
+ case AlignBottom:
+ _, dispHeight := w.Display.Size()
+ y1 = dispHeight - y1
+ y0 = y1 - outerHeight + 2
+ default:
+ panic("invalid alignment")
+ }
+
+ inverseC := color.RGBA{
+ R: ^c.R,
+ G: ^c.G,
+ B: ^c.B,
+ A: c.A,
+ }
+ _ = tinydraw.FilledRectangle(w.Display, x0, y0, int16(outerWidth+2), outerHeight, c)
+ tinyfont.WriteLine(w.Display, w.Font, x1, y1, text, inverseC)
+}
diff --git a/experimental/knobbank/knobbank.go b/experimental/knobbank/knobbank.go
new file mode 100644
index 0000000..9f7a062
--- /dev/null
+++ b/experimental/knobbank/knobbank.go
@@ -0,0 +1,94 @@
+package knobbank
+
+import (
+ "errors"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+type KnobBank struct {
+ hal.KnobInput
+ current int
+ lastValue float32
+ bank []knobBankEntry
+}
+
+func NewKnobBank(knob hal.KnobInput, opts ...KnobBankOption) (*KnobBank, error) {
+ if knob == nil {
+ return nil, errors.New("knob is nil")
+ }
+
+ kb := &KnobBank{
+ KnobInput: knob,
+ lastValue: knob.ReadVoltage(),
+ }
+
+ for _, opt := range opts {
+ if err := opt(kb); err != nil {
+ return nil, err
+ }
+ }
+
+ return kb, nil
+}
+
+func (kb *KnobBank) Configure(config hal.AnalogInputConfig) error {
+ // Configure call on a KnobBank is not allowed
+ return errors.New("unsupported")
+ //return kb.knob.Configure(config)
+}
+
+func (kb *KnobBank) CurrentName() string {
+ if len(kb.bank) == 0 {
+ return ""
+ }
+ return kb.bank[kb.current].name
+}
+
+func (kb *KnobBank) CurrentIndex() int {
+ return kb.current
+}
+
+func (kb *KnobBank) Current() hal.KnobInput {
+ return kb
+}
+
+func (kb *KnobBank) ReadVoltage() float32 {
+ value := kb.KnobInput.ReadVoltage()
+ if len(kb.bank) == 0 {
+ return value
+ }
+
+ cur := &kb.bank[kb.current]
+ percent := kb.Percent()
+ kb.lastValue = cur.update(percent, value, kb.lastValue)
+ return cur.Value()
+}
+
+func (kb *KnobBank) Percent() float32 {
+ percent := kb.KnobInput.Percent()
+ if len(kb.bank) == 0 {
+ return percent
+ }
+
+ cur := &kb.bank[kb.current]
+ value := kb.KnobInput.ReadVoltage()
+ kb.lastValue = cur.update(percent, value, kb.lastValue)
+ return cur.Percent()
+}
+
+func (kb *KnobBank) Next() {
+ if len(kb.bank) == 0 {
+ kb.current = 0
+ return
+ }
+
+ cur := &kb.bank[kb.current]
+ cur.lock(kb.KnobInput, kb.lastValue)
+
+ kb.current++
+ if kb.current >= len(kb.bank) {
+ kb.current = 0
+ }
+ kb.bank[kb.current].unlock()
+}
diff --git a/experimental/knobbank/knobbankentry.go b/experimental/knobbank/knobbankentry.go
new file mode 100644
index 0000000..0fd7c3b
--- /dev/null
+++ b/experimental/knobbank/knobbankentry.go
@@ -0,0 +1,62 @@
+package knobbank
+
+import (
+ "math"
+
+ "github.com/awonak/EuroPiGo/clamp"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+type knobBankEntry struct {
+ name string
+ enabled bool
+ locked bool
+ value float32
+ percent float32
+ vlerp lerp.Lerper32[float32]
+ minVoltage float32
+ maxVoltage float32
+ scale float32
+}
+
+func (e *knobBankEntry) lock(knob hal.KnobInput, lastValue float32) float32 {
+ if e.locked {
+ return lastValue
+ }
+
+ e.locked = true
+ value := knob.ReadVoltage()
+ percent := knob.Percent()
+ return e.update(percent, value, lastValue)
+}
+
+func (e *knobBankEntry) unlock() {
+ if !e.enabled {
+ return
+ }
+
+ e.locked = false
+}
+
+func (e *knobBankEntry) Percent() float32 {
+ return clamp.Clamp(e.percent*e.scale, 0, 1)
+}
+
+func (e *knobBankEntry) Value() float32 {
+ return clamp.Clamp(e.value*e.scale, e.minVoltage, e.maxVoltage)
+}
+
+func (e *knobBankEntry) update(percent, value, lastValue float32) float32 {
+ if !e.enabled || e.locked {
+ return lastValue
+ }
+
+ if math.Abs(float64(value-lastValue)) < 0.05 {
+ return lastValue
+ }
+
+ e.percent = percent
+ e.value = value
+ return value
+}
diff --git a/experimental/knobbank/knobbankoptions.go b/experimental/knobbank/knobbankoptions.go
new file mode 100644
index 0000000..b9ee72b
--- /dev/null
+++ b/experimental/knobbank/knobbankoptions.go
@@ -0,0 +1,41 @@
+package knobbank
+
+import (
+ "fmt"
+)
+
+type KnobBankOption func(kb *KnobBank) error
+
+func WithDisabledKnob() KnobBankOption {
+ return func(kb *KnobBank) error {
+ kb.bank = append(kb.bank, knobBankEntry{})
+ return nil
+ }
+}
+
+const (
+ defaultMinInputVoltage = 0.0
+ defaultMaxInputVoltage = 10.0
+)
+
+func WithLockedKnob(name string, opts ...KnobOption) KnobBankOption {
+ return func(kb *KnobBank) error {
+ e := knobBankEntry{
+ name: name,
+ enabled: true,
+ locked: true,
+ minVoltage: defaultMinInputVoltage,
+ maxVoltage: defaultMaxInputVoltage,
+ scale: 1,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&e); err != nil {
+ return fmt.Errorf("%s knob configuration error: %w", name, err)
+ }
+ }
+
+ kb.bank = append(kb.bank, e)
+ return nil
+ }
+}
diff --git a/experimental/knobbank/knoboptions.go b/experimental/knobbank/knoboptions.go
new file mode 100644
index 0000000..b5e6142
--- /dev/null
+++ b/experimental/knobbank/knoboptions.go
@@ -0,0 +1,40 @@
+package knobbank
+
+import (
+ "fmt"
+
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+type KnobOption func(e *knobBankEntry) error
+
+func InitialPercentageValue(v float32) KnobOption {
+ return func(e *knobBankEntry) error {
+ if v < 0 || v > 1 {
+ return fmt.Errorf("initial percentage value of %f is outside the range [0..1]", v)
+ }
+
+ e.percent = v
+ e.vlerp = lerp.NewLerp32[float32](defaultMinInputVoltage, defaultMaxInputVoltage)
+ e.value = e.vlerp.ClampedLerp(v)
+ return nil
+ }
+}
+
+func MinInputVoltage(v float32) KnobOption {
+ return func(e *knobBankEntry) error {
+ e.minVoltage = v
+ e.vlerp = lerp.NewLerp32(e.minVoltage, e.maxVoltage)
+ e.value = e.vlerp.ClampedLerp(e.percent)
+ return nil
+ }
+}
+
+func MaxInputVoltage(v float32) KnobOption {
+ return func(e *knobBankEntry) error {
+ e.maxVoltage = v
+ e.vlerp = lerp.NewLerp32(e.minVoltage, e.maxVoltage)
+ e.value = e.vlerp.ClampedLerp(e.percent)
+ return nil
+ }
+}
diff --git a/go.mod b/go.mod
index e221ece..894749b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,10 @@
module github.com/awonak/EuroPiGo
-go 1.19
+go 1.18
require (
- tinygo.org/x/drivers v0.22.0
+ github.com/gorilla/websocket v1.5.0
+ tinygo.org/x/drivers v0.24.0
tinygo.org/x/tinydraw v0.3.0
tinygo.org/x/tinyfont v0.3.0
)
diff --git a/go.sum b/go.sum
index ceddd07..700f6e8 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ github.com/bgould/http v0.0.0-20190627042742-d268792bdee7/go.mod h1:BTqvVegvwifo
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hajimehoshi/go-jisx0208 v1.0.0/go.mod h1:yYxEStHL7lt9uL+AbdWgW9gBumwieDoZCiB1f/0X0as=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -20,8 +22,8 @@ tinygo.org/x/drivers v0.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2
tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI=
tinygo.org/x/drivers v0.16.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI=
tinygo.org/x/drivers v0.19.0/go.mod h1:uJD/l1qWzxzLx+vcxaW0eY464N5RAgFi1zTVzASFdqI=
-tinygo.org/x/drivers v0.22.0 h1:s5c0hJY8pJYojSGS5AQgauwhTuH2bBZPDqdwkBDGW+o=
-tinygo.org/x/drivers v0.22.0/go.mod h1:J4+51Li1kcfL5F93kmnDWEEzQF3bLGz0Am3Q7E2a8/E=
+tinygo.org/x/drivers v0.24.0 h1:9yOV/GbdDXg+EKOWJl+FHLKMSfSI/skQ8Gmo8N/KzQo=
+tinygo.org/x/drivers v0.24.0/go.mod h1:J4+51Li1kcfL5F93kmnDWEEzQF3bLGz0Am3Q7E2a8/E=
tinygo.org/x/tinydraw v0.3.0 h1:OjsdMcES5+7IIs/4diFpq/pWFsa0VKtbi1mURuj2q64=
tinygo.org/x/tinydraw v0.3.0/go.mod h1:Yz0vLSP2rHsIKpLYkEmLnE+2zyhhITu2LxiVtLRiW6I=
tinygo.org/x/tinyfont v0.2.1/go.mod h1:eLqnYSrFRjt5STxWaMeOWJTzrKhXqpWw7nU3bPfKOAM=
diff --git a/hardware/README.md b/hardware/README.md
new file mode 100644
index 0000000..db1517c
--- /dev/null
+++ b/hardware/README.md
@@ -0,0 +1,40 @@
+# hardware
+
+This package is used for obtaining singleton objects for particular hardware, identified by `Revision` and `HardwareId`.
+
+## Revision List
+
+**NOTE**: The full revision list may be found under [hal/revision.go](hal/revision.go).
+
+| Identifier | Alias | (non-pico) Build Flags | Notes |
+|----|----|----|----|
+| `Revision0` | `EuroPiProto` | | EuroPi prototype developed before revision 1 design was solidified. Due to lack of examples of this hardware, support is limited or non-existent. |
+| `Revision1` | `EuroPi` | `revision1` or `europi` | EuroPi 'production' release, revision 1. |
+| `Revision2` | `EuroPiX` | `revision2` or `europix` | EuroPi X - an improved hardware revision of the EuroPi. Currently in pre-production development hell. |
+
+## Hardware List
+
+**NOTE**: The full hardware list may be found under [hal/hardware.go](hal/hardware.go).
+
+Calling `GetHardware()` with a `Revision` (see above) and `HardwareId` (see below) may return a singleton hardware interface. Check to see if the returned value is `nil` before using it; `nil` is considered to be either a revision detection failure, missing hardware, or some other error.
+
+| HardwareId | HardwareId Alias | Interface | EuroPi Prototype | EuroPi | EuroPi-X | Notes |
+|----|----|----|----|----|----|----|
+| `HardwareIdInvalid` | | N/A | | | | Always returns a `nil` interface/object |
+| `HardwareIdRevisionMarker` | | `hal.RevisionMarker` | | | | Provides an interface to obtain the `Revision` identifier of the currently detected (or compiled-for) hardware. |
+| `HardwareIdDigital1Input` | | `hal.DigitalInput` | | `InputDigital1` | | The Digital Input of the EuroPi. |
+| `HardwareIdAnalog1Input` | `HardwareIdAnalogue1Input` | `hal.AnalogInput` | | `InputAnalog1` | | The Analogue Input of the EuroPi. |
+| `HardwareIdDisplay1Output` | | `hal.DisplayOutput` | | `OutputDisplay1` | | The Display (OLED) of the EuroPi. Provides an interface for determining display resolution, as it might be different between revisions of the EuroPi hardware. |
+| `HardwareIdButton1Input` | | `hal.ButtonInput` | `InputButton1` | `InputButton1` | | The Button 1 gate input of the EuroPi. |
+| `HardwareIdButton2Input` | | `hal.ButtonInput` | `InputButton2` | `InputButton2` | | The Button 2 gate input of the EuroPi. |
+| `HardwareIdKnob1Input` | | `hal.KnobInput` | `InputKnob1` | `InputKnob1` | | The Knob 1 potentiometer input of the EuroPi. |
+| `HardwareIdKnob2Input` | | `hal.KnobInput` | `InputKnob2` | `InputKnob2` | | The Knob 2 potentiometer input of the EuroPi. |
+| `HardwareIdVoltage1Output` | | `hal.VoltageOutput` | `OutputAnalog1` | `OutputVoltage1` | | The #1 `CV` / `V/Octave` output of the EuroPi. While the EuroPi supports a 0.0 to 10.0 Volts output necessary for `V/Octave` (see `units.VOct`), it can be carefully used with `units.CV` to output a specific range of 0.0 to 5.0 Volts, instead. For the EuroPi Prototype, the range is 0.0 to 3.3 Volts. |
+| `HardwareIdVoltage2Output` | | `hal.VoltageOutput` | `OutputAnalog2` | `OutputVoltage2` | | The #2 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. |
+| `HardwareIdVoltage3Output` | | `hal.VoltageOutput` | `OutputAnalog3` | `OutputVoltage3` | | The #3 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. |
+| `HardwareIdVoltage4Output` | | `hal.VoltageOutput` | `OutputAnalog4` | `OutputVoltage4` | | The #4 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. |
+| `HardwareIdVoltage5Output` | | `hal.VoltageOutput` | `OutputDigital1` | `OutputVoltage5` | | The #5 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. |
+| `HardwareIdVoltage6Output` | | `hal.VoltageOutput` | `OutputDigital2` | `OutputVoltage6` | | The #6 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. |
+| `HardwareIdRandom1Generator` | | `hal.RandomGenerator` | `DeviceRandomGenerator1` | `DeviceRandomGenerator1` | | Provides an interface to calibrate or seed the random number generator of the hardware. |
+| `HardwareIdVoltage7Output` | | `hal.VoltageOutput` | `OutputDigital3` | | | The #7 `CV` / `V/Octave` output of the EuroPi Prototype. See `HardwareIdVoltage1Output` for more details. |
+| `HardwareIdVoltage8Output` | | `hal.VoltageOutput` | `OutputDigital4` | | | The #8 `CV` / `V/Octave` output of the EuroPi Prototype. See `HardwareIdVoltage1Output` for more details. |
diff --git a/hardware/common/analoginput.go b/hardware/common/analoginput.go
new file mode 100644
index 0000000..0ceb501
--- /dev/null
+++ b/hardware/common/analoginput.go
@@ -0,0 +1,103 @@
+package common
+
+import (
+ "errors"
+
+ "github.com/awonak/EuroPiGo/clamp"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/lerp"
+ "github.com/awonak/EuroPiGo/units"
+)
+
+// Analog is a struct for handling the reading of analogue control voltage.
+// The analogue input allows you to 'read' CV from anywhere between 0 and 12V.
+type Analoginput struct {
+ adc ADCProvider
+ samples int
+ cal lerp.Remapper32[uint16, float32]
+}
+
+var (
+ // static check
+ _ hal.AnalogInput = (*Analoginput)(nil)
+ // silence linter
+ _ = NewAnalogInput
+)
+
+type ADCProvider interface {
+ Get(samples int) uint16
+}
+
+// NewAnalogInput creates a new Analog Input
+func NewAnalogInput(adc ADCProvider, initialConfig hal.AnalogInputConfig) *Analoginput {
+ if adc == nil {
+ return nil
+ }
+ return &Analoginput{
+ adc: adc,
+ samples: initialConfig.Samples,
+ cal: initialConfig.Calibration,
+ }
+}
+
+// Configure updates the device with various configuration parameters
+func (a *Analoginput) Configure(config hal.AnalogInputConfig) error {
+ if config.Samples == 0 {
+ return errors.New("samples must be non-zero")
+ }
+
+ if config.Calibration != nil {
+ a.cal = config.Calibration
+ }
+
+ a.samples = config.Samples
+ return nil
+}
+
+// ReadRawVoltage returns the current smoothed value from the analog input device.
+func (a *Analoginput) ReadRawVoltage() uint16 {
+ return a.adc.Get(a.samples)
+}
+
+// ReadVoltage returns the current percentage read between 0.0 and 1.0.
+func (a *Analoginput) Percent() float32 {
+ return a.ReadVoltage() / a.cal.OutputMaximum()
+}
+
+// ReadVoltage returns the current read voltage between 0.0 and 10.0 volts.
+func (a *Analoginput) ReadVoltage() float32 {
+ rawVoltage := a.ReadRawVoltage()
+ return a.cal.Remap(rawVoltage)
+}
+
+// ReadCV returns the current read voltage as a CV value.
+func (a *Analoginput) ReadCV() units.CV {
+ v := a.ReadVoltage()
+ // CV is ranged over 0.0V .. +5.0V and stores the values as a normalized
+ // version (0.0 .. +1.0), so to convert our input voltage to that, we just
+ // normalize the voltage (divide it by 5) and clamp the result.
+ return clamp.Clamp(units.CV(v/5.0), 0.0, 1.0)
+}
+
+func (a *Analoginput) ReadBipolarCV() units.BipolarCV {
+ v := a.ReadVoltage()
+ // BipolarCV is ranged over -5.0V .. +5.0V and stores the values as a normalized
+ // version (-1.0 .. +1.0), so to convert our input voltage to that, we just
+ // normalize the voltage (divide it by 5) and clamp the result.
+ return clamp.Clamp(units.BipolarCV(v/5.0), -1.0, 1.0)
+}
+
+// ReadCV returns the current read voltage as a V/Octave value.
+func (a *Analoginput) ReadVOct() units.VOct {
+ return units.VOct(a.ReadVoltage())
+}
+
+// MinVoltage returns the minimum voltage that that input can ever read by this device
+func (a *Analoginput) MinVoltage() float32 {
+ return a.cal.OutputMinimum()
+}
+
+// MaxVoltage returns the maximum voltage that the input can ever read by this device
+func (a *Analoginput) MaxVoltage() float32 {
+ return a.cal.OutputMaximum()
+}
diff --git a/hardware/common/contextpi.go b/hardware/common/contextpi.go
new file mode 100644
index 0000000..b5bf084
--- /dev/null
+++ b/hardware/common/contextpi.go
@@ -0,0 +1,141 @@
+package common
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "sync/atomic"
+)
+
+// ContextPi gives the EuroPi hardware the components necessary
+// to perform rudimentary context operations
+type ContextPi struct {
+ context.Context
+ mu sync.Mutex // protects following fields
+ done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
+ children map[canceler]struct{} // set to nil by the first cancel call
+ err error // set to non-nil by the first cancel call
+}
+
+func (c *ContextPi) Done() <-chan struct{} {
+ d := c.done.Load()
+ if d != nil {
+ return d.(chan struct{})
+ }
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ d = c.done.Load()
+ if d == nil {
+ d = make(chan struct{})
+ c.done.Store(d)
+ }
+ return d.(chan struct{})
+}
+
+func (c *ContextPi) Err() error {
+ c.mu.Lock()
+ err := c.err
+ c.mu.Unlock()
+ return err
+}
+
+func (c *ContextPi) Value(key any) any {
+ if key == &cancelCtxKey {
+ return c
+ }
+ return c.Context.Value(key)
+}
+
+// cancel closes c.done, cancels each of c's children, and, if
+// removeFromParent is true, removes c from its parent's children.
+func (c *ContextPi) cancel(removeFromParent bool, err error) {
+ if err == nil {
+ panic("context: internal error: missing cancel error")
+ }
+ c.mu.Lock()
+ if c.err != nil {
+ c.mu.Unlock()
+ return // already canceled
+ }
+ c.err = err
+ d, _ := c.done.Load().(chan struct{})
+ if d == nil {
+ c.done.Store(closedchan)
+ } else {
+ close(d)
+ }
+ for child := range c.children {
+ // NOTE: acquiring the child's lock while holding parent's lock.
+ child.cancel(false, err)
+ }
+ c.children = nil
+ c.mu.Unlock()
+
+ if removeFromParent {
+ removeChild(c.Context, c)
+ }
+}
+
+// &cancelCtxKey is the key that a cancelCtx returns itself for.
+var cancelCtxKey int
+
+// parentCancelCtx returns the underlying *cancelCtx for parent.
+// It does this by looking up parent.Value(&cancelCtxKey) to find
+// the innermost enclosing *cancelCtx and then checking whether
+// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
+// has been wrapped in a custom implementation providing a
+// different done channel, in which case we should not bypass it.)
+func parentCancelCtx(parent context.Context) (*ContextPi, bool) {
+ if parent == nil {
+ return nil, false
+ }
+ done := parent.Done()
+ if done == closedchan || done == nil {
+ return nil, false
+ }
+ p, ok := parent.Value(&cancelCtxKey).(*ContextPi)
+ if !ok {
+ return nil, false
+ }
+ pdone, _ := p.done.Load().(chan struct{})
+ if pdone != done {
+ return nil, false
+ }
+ return p, true
+}
+
+// removeChild removes a context from its parent.
+func removeChild(parent context.Context, child canceler) {
+ p, ok := parentCancelCtx(parent)
+ if !ok {
+ return
+ }
+ p.mu.Lock()
+ if p.children != nil {
+ delete(p.children, child)
+ }
+ p.mu.Unlock()
+}
+
+func (c *ContextPi) Shutdown(reason any) error {
+ var err error
+ if reason != nil {
+ err = fmt.Errorf("%v", reason)
+ }
+ c.cancel(true, err)
+ return nil
+}
+
+// A canceler is a context type that can be canceled directly. The
+// implementation is *ContextPi
+type canceler interface {
+ cancel(removeFromParent bool, err error)
+ Done() <-chan struct{}
+}
+
+// closedchan is a reusable closed channel.
+var closedchan = make(chan struct{})
+
+func init() {
+ close(closedchan)
+}
diff --git a/hardware/common/digitalinput.go b/hardware/common/digitalinput.go
new file mode 100644
index 0000000..c030ac2
--- /dev/null
+++ b/hardware/common/digitalinput.go
@@ -0,0 +1,72 @@
+package common
+
+import (
+ "time"
+
+ "github.com/awonak/EuroPiGo/debounce"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+// Digitalinput is a struct for handling reading of the digital input.
+type Digitalinput struct {
+ dr DigitalReaderProvider
+ lastChange time.Time
+}
+
+var (
+ // static check
+ _ hal.DigitalInput = (*Digitalinput)(nil)
+ // silence linter
+ _ = NewDigitalInput
+)
+
+type DigitalReaderProvider interface {
+ Get() bool
+ SetHandler(changes hal.ChangeFlags, handler func())
+}
+
+// NewDigitalInput creates a new digital input struct.
+func NewDigitalInput(dr DigitalReaderProvider) *Digitalinput {
+ if dr == nil {
+ return nil
+ }
+ return &Digitalinput{
+ dr: dr,
+ lastChange: time.Now(),
+ }
+}
+
+// Configure updates the device with various configuration parameters
+func (d *Digitalinput) Configure(config hal.DigitalInputConfig) error {
+ return nil
+}
+
+// Value returns true if the input is high (above 0.8v), else false.
+func (d *Digitalinput) Value() bool {
+ return d.dr.Get()
+}
+
+// Handler sets the callback function to be call when the incoming signal going high event is detected.
+func (d *Digitalinput) Handler(handler func(value bool, deltaTime time.Duration)) {
+ d.HandlerEx(hal.ChangeRising, handler)
+}
+
+// HandlerEx sets the callback function to be call when the input changes in a specified way.
+func (d *Digitalinput) HandlerEx(changes hal.ChangeFlags, handler func(value bool, deltaTime time.Duration)) {
+ d.dr.SetHandler(changes, func() {
+ now := time.Now()
+ timeDelta := now.Sub(d.lastChange)
+ handler(d.Value(), timeDelta)
+ d.lastChange = now
+ })
+}
+
+// HandlerWithDebounce sets the callback function to be call when the incoming signal going high event is detected and debounce delay time has elapsed.
+func (d *Digitalinput) HandlerWithDebounce(handler func(value bool, deltaTime time.Duration), delay time.Duration) {
+ db := debounce.NewDebouncer(handler).Debounce(delay)
+ d.Handler(func(value bool, _ time.Duration) {
+ // throw away the deltaTime coming in on the handler
+ // we want to use what's on the debouncer, instead
+ db(value)
+ })
+}
diff --git a/hardware/common/displayoutput.go b/hardware/common/displayoutput.go
new file mode 100644
index 0000000..dc32455
--- /dev/null
+++ b/hardware/common/displayoutput.go
@@ -0,0 +1,62 @@
+package common
+
+import (
+ "image/color"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+// DisplayOutput is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED.
+type DisplayOutput struct {
+ dp DisplayProvider
+}
+
+var (
+ // static check
+ _ hal.DisplayOutput = (*DisplayOutput)(nil)
+ // silence linter
+ _ = NewDisplayOutput
+)
+
+type DisplayProvider interface {
+ ClearBuffer()
+ Size() (x, y int16)
+ SetPixel(x, y int16, c color.RGBA)
+ Display() error
+}
+
+// NewDisplayOutput returns a new Display struct.
+func NewDisplayOutput(dp DisplayProvider) *DisplayOutput {
+ if dp == nil {
+ return nil
+ }
+ return &DisplayOutput{
+ dp: dp,
+ }
+}
+
+// Configure updates the device with various configuration parameters
+func (d *DisplayOutput) Configure(config hal.DisplayOutputConfig) error {
+ return nil
+}
+
+// ClearBuffer clears the internal display buffer for the device
+func (d *DisplayOutput) ClearBuffer() {
+ d.dp.ClearBuffer()
+}
+
+// Size returns the display resolution for the device
+func (d *DisplayOutput) Size() (x, y int16) {
+ return d.dp.Size()
+}
+
+// SetPixel sets a specific pixel at coordinates (`x`,`y`) to color `c`.
+func (d *DisplayOutput) SetPixel(x, y int16, c color.RGBA) {
+ d.dp.SetPixel(x, y, c)
+}
+
+// Display commits the internal buffer to the display device.
+// This will update the physical content displayed on the device.
+func (d *DisplayOutput) Display() error {
+ return d.dp.Display()
+}
diff --git a/hardware/common/randomgenerator.go b/hardware/common/randomgenerator.go
new file mode 100644
index 0000000..d452f4b
--- /dev/null
+++ b/hardware/common/randomgenerator.go
@@ -0,0 +1,37 @@
+package common
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+type RandomGenerator struct {
+ rnd RNDProvider
+}
+
+var (
+ // static check
+ _ hal.RandomGenerator = (*RandomGenerator)(nil)
+ // silence linter
+ _ = NewRandomGenerator
+)
+
+func NewRandomGenerator(rnd RNDProvider) *RandomGenerator {
+ return &RandomGenerator{
+ rnd: rnd,
+ }
+}
+
+type RNDProvider interface {
+ Configure(config hal.RandomGeneratorConfig) error
+}
+
+// Configure updates the device with various configuration parameters
+func (r *RandomGenerator) Configure(config hal.RandomGeneratorConfig) error {
+ if r.rnd != nil {
+ if err := r.rnd.Configure(config); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/hardware/common/voltageoutput.go b/hardware/common/voltageoutput.go
new file mode 100644
index 0000000..3be76ef
--- /dev/null
+++ b/hardware/common/voltageoutput.go
@@ -0,0 +1,85 @@
+package common
+
+import (
+ "fmt"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/units"
+)
+
+// VoltageOutput is struct for interacting with the CV/VOct voltage output jacks.
+type VoltageOutput struct {
+ pwm PWMProvider
+}
+
+var (
+ // static check
+ _ hal.VoltageOutput = (*VoltageOutput)(nil)
+ // silence linter
+ _ = NewVoltageOuput
+)
+
+type PWMProvider interface {
+ Configure(config hal.VoltageOutputConfig) error
+ Set(v float32)
+ Get() float32
+ MinVoltage() float32
+ MaxVoltage() float32
+}
+
+// NewOutput returns a new Output interface.
+func NewVoltageOuput(pwm PWMProvider, initialConfig hal.VoltageOutputConfig) *VoltageOutput {
+ o := &VoltageOutput{
+ pwm: pwm,
+ }
+ err := o.Configure(initialConfig)
+ if err != nil {
+ panic(fmt.Errorf("configuration error: %v", err.Error()))
+ }
+
+ return o
+}
+
+// Configure updates the device with various configuration parameters
+func (o *VoltageOutput) Configure(config hal.VoltageOutputConfig) error {
+ if err := o.pwm.Configure(config); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// SetVoltage sets the current output voltage within a range of 0.0 to 10.0.
+func (o *VoltageOutput) SetVoltage(v float32) {
+ o.pwm.Set(v)
+}
+
+// SetCV sets the current output voltage based on a CV value
+func (o *VoltageOutput) SetCV(cv units.CV) {
+ o.SetVoltage(cv.ToVolts())
+}
+
+// SetBipolarCV sets the current output voltage based on a BipolarCV value
+func (o *VoltageOutput) SetBipolarCV(cv units.BipolarCV) {
+ o.SetVoltage(cv.ToVolts())
+}
+
+// SetCV sets the current output voltage based on a V/Octave value
+func (o *VoltageOutput) SetVOct(voct units.VOct) {
+ o.SetVoltage(voct.ToVolts())
+}
+
+// Voltage returns the current voltage
+func (o *VoltageOutput) Voltage() float32 {
+ return o.pwm.Get()
+}
+
+// MinVoltage returns the minimum voltage this device will output
+func (o *VoltageOutput) MinVoltage() float32 {
+ return o.pwm.MinVoltage()
+}
+
+// MaxVoltage returns the maximum voltage this device will output
+func (o *VoltageOutput) MaxVoltage() float32 {
+ return o.pwm.MaxVoltage()
+}
diff --git a/hardware/hal/analoginput.go b/hardware/hal/analoginput.go
new file mode 100644
index 0000000..26b3fcf
--- /dev/null
+++ b/hardware/hal/analoginput.go
@@ -0,0 +1,23 @@
+package hal
+
+import (
+ "github.com/awonak/EuroPiGo/lerp"
+ "github.com/awonak/EuroPiGo/units"
+)
+
+type AnalogInput interface {
+ Configure(config AnalogInputConfig) error
+ Percent() float32
+ ReadRawVoltage() uint16
+ ReadVoltage() float32
+ ReadCV() units.CV
+ ReadBipolarCV() units.BipolarCV
+ ReadVOct() units.VOct
+ MinVoltage() float32
+ MaxVoltage() float32
+}
+
+type AnalogInputConfig struct {
+ Samples int
+ Calibration lerp.Remapper32[uint16, float32]
+}
diff --git a/hardware/hal/buttoninput.go b/hardware/hal/buttoninput.go
new file mode 100644
index 0000000..ca67a05
--- /dev/null
+++ b/hardware/hal/buttoninput.go
@@ -0,0 +1,3 @@
+package hal
+
+type ButtonInput = DigitalInput
diff --git a/hardware/hal/changes.go b/hardware/hal/changes.go
new file mode 100644
index 0000000..8c300c5
--- /dev/null
+++ b/hardware/hal/changes.go
@@ -0,0 +1,13 @@
+package hal
+
+type ChangeFlags int
+
+const (
+ ChangeRising = ChangeFlags(1 << iota)
+ ChangeFalling
+)
+
+const (
+ ChangeNone = ChangeFlags(0)
+ ChangeAny = ChangeRising | ChangeFalling
+)
diff --git a/hardware/hal/digitalinput.go b/hardware/hal/digitalinput.go
new file mode 100644
index 0000000..dd208c0
--- /dev/null
+++ b/hardware/hal/digitalinput.go
@@ -0,0 +1,17 @@
+package hal
+
+import "time"
+
+type DigitalInput interface {
+ Configure(config DigitalInputConfig) error
+ Value() bool
+ Handler(handler func(value bool, deltaTime time.Duration))
+ HandlerEx(changes ChangeFlags, handler func(value bool, deltaTime time.Duration))
+ HandlerWithDebounce(handler func(value bool, deltaTime time.Duration), delay time.Duration)
+}
+
+type DigitalInputConfig struct {
+ Samples uint16
+ CalibratedMinAI uint16
+ CalibratedMaxAI uint16
+}
diff --git a/hardware/hal/displayoutput.go b/hardware/hal/displayoutput.go
new file mode 100644
index 0000000..8d396af
--- /dev/null
+++ b/hardware/hal/displayoutput.go
@@ -0,0 +1,13 @@
+package hal
+
+import "image/color"
+
+type DisplayOutput interface {
+ ClearBuffer()
+ Size() (x, y int16)
+ SetPixel(x, y int16, c color.RGBA)
+ Display() error
+}
+
+type DisplayOutputConfig struct {
+}
diff --git a/hardware/hal/hardware.go b/hardware/hal/hardware.go
new file mode 100644
index 0000000..09bfb8b
--- /dev/null
+++ b/hardware/hal/hardware.go
@@ -0,0 +1,43 @@
+package hal
+
+import "context"
+
+// HardwareId defines an identifier for specific hardware. See the README.md in the hardware directory for more details.
+type HardwareId int
+
+const (
+ HardwareIdInvalid = HardwareId(iota)
+ HardwareIdRevisionMarker
+ HardwareIdDigital1Input
+ HardwareIdAnalog1Input
+ HardwareIdDisplay1Output
+ HardwareIdButton1Input
+ HardwareIdButton2Input
+ HardwareIdKnob1Input
+ HardwareIdKnob2Input
+ HardwareIdVoltage1Output
+ HardwareIdVoltage2Output
+ HardwareIdVoltage3Output
+ HardwareIdVoltage4Output
+ HardwareIdVoltage5Output
+ HardwareIdVoltage6Output
+ HardwareIdRandom1Generator
+ HardwareIdVoltage7Output
+ HardwareIdVoltage8Output
+ // NOTE: always ONLY append to this list, NEVER remove, rename, or reorder
+)
+
+// aliases for friendly internationali(s|z)ation, colloquialisms, and naming conventions
+const (
+ HardwareIdAnalogue1Input = HardwareIdAnalog1Input
+)
+
+// Hardware is the collection of component wrappers used to interact with the module.
+type Hardware interface {
+ Context() context.Context
+ Shutdown(reason any) error
+ Revision() Revision
+ Random() RandomGenerator
+ Button(idx int) ButtonInput
+ Knob(idx int) KnobInput
+}
diff --git a/hardware/hal/knobinput.go b/hardware/hal/knobinput.go
new file mode 100644
index 0000000..888a8ec
--- /dev/null
+++ b/hardware/hal/knobinput.go
@@ -0,0 +1,3 @@
+package hal
+
+type KnobInput = AnalogInput
diff --git a/hardware/hal/randomgenerator.go b/hardware/hal/randomgenerator.go
new file mode 100644
index 0000000..93a194e
--- /dev/null
+++ b/hardware/hal/randomgenerator.go
@@ -0,0 +1,7 @@
+package hal
+
+type RandomGenerator interface {
+ Configure(config RandomGeneratorConfig) error
+}
+
+type RandomGeneratorConfig struct{}
diff --git a/hardware/hal/revision.go b/hardware/hal/revision.go
new file mode 100644
index 0000000..af90822
--- /dev/null
+++ b/hardware/hal/revision.go
@@ -0,0 +1,19 @@
+package hal
+
+// Revision defines an identifier for hardware platforms. See the README.md in the hardware directory for more details.
+type Revision int
+
+const (
+ RevisionUnknown = Revision(iota)
+ Revision0
+ Revision1
+ Revision2
+ // NOTE: always ONLY append to this list, NEVER remove, rename, or reorder
+)
+
+// aliases
+const (
+ EuroPiProto = Revision0
+ EuroPi = Revision1
+ EuroPiX = Revision2
+)
diff --git a/hardware/hal/revisionmarker.go b/hardware/hal/revisionmarker.go
new file mode 100644
index 0000000..68049eb
--- /dev/null
+++ b/hardware/hal/revisionmarker.go
@@ -0,0 +1,31 @@
+package hal
+
+type RevisionMarker interface {
+ Revision() Revision
+}
+
+type revisionMarker struct {
+ detectedRevision Revision
+}
+
+var (
+ // static check
+ _ RevisionMarker = &revisionMarker{}
+
+ RevisionMark RevisionMarker
+)
+
+func NewRevisionMark(opts ...Revision) RevisionMarker {
+ r := &revisionMarker{
+ detectedRevision: RevisionUnknown,
+ }
+ if len(opts) > 0 {
+ r.detectedRevision = opts[0]
+ }
+ return r
+}
+
+// Revision returns the detected revision of the current hardware
+func (r revisionMarker) Revision() Revision {
+ return r.detectedRevision
+}
diff --git a/hardware/hal/voltageoutput.go b/hardware/hal/voltageoutput.go
new file mode 100644
index 0000000..834ba1d
--- /dev/null
+++ b/hardware/hal/voltageoutput.go
@@ -0,0 +1,24 @@
+package hal
+
+import (
+ "time"
+
+ "github.com/awonak/EuroPiGo/lerp"
+ "github.com/awonak/EuroPiGo/units"
+)
+
+type VoltageOutput interface {
+ SetVoltage(v float32)
+ SetCV(cv units.CV)
+ SetBipolarCV(cv units.BipolarCV)
+ SetVOct(voct units.VOct)
+ Voltage() float32
+ MinVoltage() float32
+ MaxVoltage() float32
+}
+
+type VoltageOutputConfig struct {
+ Period time.Duration
+ Monopolar bool
+ Calibration lerp.Remapper32[float32, uint16]
+}
diff --git a/hardware/platform.go b/hardware/platform.go
new file mode 100644
index 0000000..515a5cf
--- /dev/null
+++ b/hardware/platform.go
@@ -0,0 +1,67 @@
+package hardware
+
+import (
+ "sync"
+ "sync/atomic"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/hardware/rev0"
+ "github.com/awonak/EuroPiGo/hardware/rev1"
+)
+
+// GetHardware returns a hardware device based on EuroPi `revision` and hardware `id`.
+// a `nil` result means that the hardware was not found or some sort of error occurred.
+func GetHardware[T any](revision hal.Revision, id hal.HardwareId) T {
+ switch revision {
+ case hal.Revision0:
+ return rev0.GetHardware[T](id)
+
+ case hal.Revision1:
+ return rev1.GetHardware[T](id)
+
+ case hal.Revision2:
+ // TODO: implement hardware design of rev2
+ //return rev2.GetHardware[T](id)
+ fallthrough
+
+ default:
+ var none T
+ return none
+ }
+}
+
+// WaitForReady awaits the readiness of the hardware initialization.
+// This will block until every aspect of hardware initialization has completed.
+func WaitForReady() {
+ ensureHardwareReady()
+ hardwareReadyCond.L.Lock()
+ for {
+ ready := hardwareReady.Load()
+ if v, ok := ready.(bool); v && ok {
+ break
+ }
+ hardwareReadyCond.Wait()
+ }
+ hardwareReadyCond.L.Unlock()
+}
+
+var (
+ hardwareReady atomic.Value
+ hardwareReadyMu sync.Mutex
+ hardwareReadyOnce sync.Once
+ hardwareReadyCond *sync.Cond
+)
+
+func ensureHardwareReady() {
+ hardwareReadyOnce.Do(func() {
+ hardwareReadyCond = sync.NewCond(&hardwareReadyMu)
+ })
+}
+
+// SetReady is used by the hardware initialization code.
+// Do not call this function directly.
+func SetReady() {
+ ensureHardwareReady()
+ hardwareReady.Store(true)
+ hardwareReadyCond.Broadcast()
+}
diff --git a/hardware/rev0/README.md b/hardware/rev0/README.md
new file mode 100644
index 0000000..b5d8147
--- /dev/null
+++ b/hardware/rev0/README.md
@@ -0,0 +1,21 @@
+# hardware/rev1
+
+This package is used for the [Original EuroPi hardware](https://github.com/Allen-Synthesis/EuroPi/tree/main/hardware).
+
+## Hardware Mapping
+
+| Name | Interface | HardwareId | HardwareId Alias |
+|----|----|----|----|
+| `InputButton1` | `hal.ButtonInput` | `HardwareIdButton1Input` | |
+| `InputButton2` | `hal.ButtonInput` | `HardwareIdButton2Input` | |
+| `InputKnob1` | `hal.KnobInput` | `HardwareIdKnob1Input` | |
+| `InputKnob2` | `hal.KnobInput` | `HardwareIdKnob2Input` | |
+| `OutputAnalog1` | `hal.VoltageOutput` | `HardwareIdVoltage1Output` | `HardwareIdAnalog1Output` |
+| `OutputAnalog2` | `hal.VoltageOutput` | `HardwareIdVoltage2Output` | `HardwareIdAnalog2Output` |
+| `OutputAnalog3` | `hal.VoltageOutput` | `HardwareIdVoltage3Output` | `HardwareIdAnalog3Output` |
+| `OutputAnalog4` | `hal.VoltageOutput` | `HardwareIdVoltage4Output` | `HardwareIdAnalog4Output` |
+| `OutputDigital1` | `hal.VoltageOutput` | `HardwareIdVoltage5Output` | `HardwareIdDigital1Output` |
+| `OutputDigital2` | `hal.VoltageOutput` | `HardwareIdVoltage6Output` | `HardwareIdDigital2Output` |
+| `OutputDigital3` | `hal.VoltageOutput` | `HardwareIdVoltage7Output` | `HardwareIdDigital3Output` |
+| `OutputDigital4` | `hal.VoltageOutput` | `HardwareIdVoltage8Output` | `HardwareIdDigital4Output` |
+| `DeviceRandomGenerator1` | `hal.RandomGenerator` | `HardwareIdRandom1Generator` | |
diff --git a/hardware/rev0/analoginput.go b/hardware/rev0/analoginput.go
new file mode 100644
index 0000000..e0eb273
--- /dev/null
+++ b/hardware/rev0/analoginput.go
@@ -0,0 +1,27 @@
+package rev0
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+const (
+ // DefaultCalibrated[Min|Max]AI was calculated using the EuroPi calibration program:
+ // https://github.com/Allen-Synthesis/EuroPi/blob/main/software/programming_instructions.md#calibrate-the-module
+ DefaultCalibratedMinAI = 300
+ DefaultCalibratedMaxAI = 44009
+
+ DefaultSamples = 1000
+
+ MaxInputVoltage = 3.3
+ MinInputVoltage = 0.0
+)
+
+var (
+ DefaultAICalibration = lerp.NewRemap32[uint16, float32](DefaultCalibratedMinAI, DefaultCalibratedMaxAI, MinInputVoltage, MaxInputVoltage)
+
+ aiInitialConfig = hal.AnalogInputConfig{
+ Samples: DefaultSamples,
+ Calibration: DefaultAICalibration,
+ }
+)
diff --git a/hardware/rev0/hardware.go b/hardware/rev0/hardware.go
new file mode 100644
index 0000000..eca1a81
--- /dev/null
+++ b/hardware/rev0/hardware.go
@@ -0,0 +1,41 @@
+package rev0
+
+import "github.com/awonak/EuroPiGo/hardware/hal"
+
+// aliases for module revision specific referencing
+const (
+ // K1
+ HardwareIdKnob1Input = hal.HardwareIdKnob1Input
+ // K2
+ HardwareIdKnob2Input = hal.HardwareIdKnob2Input
+ // B1
+ HardwareIdButton1Input = hal.HardwareIdButton1Input
+ // B2
+ HardwareIdButton2Input = hal.HardwareIdButton2Input
+ // AJ1
+ HardwareIdAnalog1Output = hal.HardwareIdVoltage1Output
+ // AJ2
+ HardwareIdAnalog2Output = hal.HardwareIdVoltage2Output
+ // AJ3
+ HardwareIdAnalog3Output = hal.HardwareIdVoltage3Output
+ // AJ4
+ HardwareIdAnalog4Output = hal.HardwareIdVoltage4Output
+ // DJ1
+ HardwareIdDigital1Output = hal.HardwareIdVoltage5Output
+ // DJ2
+ HardwareIdDigital2Output = hal.HardwareIdVoltage6Output
+ // DJ3
+ HardwareIdDigital3Output = hal.HardwareIdVoltage7Output
+ // DJ4
+ HardwareIdDigital4Output = hal.HardwareIdVoltage8Output
+ // RNG
+ HardwareIdRandom1Generator = hal.HardwareIdRandom1Generator
+)
+
+// aliases for friendly internationali(s|z)ation, colloquialisms, and naming conventions
+const (
+ HardwareIdAnalogue1Output = HardwareIdAnalog1Output
+ HardwareIdAnalogue2Output = HardwareIdAnalog2Output
+ HardwareIdAnalogue3Output = HardwareIdAnalog3Output
+ HardwareIdAnalogue4Output = HardwareIdAnalog4Output
+)
diff --git a/hardware/rev0/platform.go b/hardware/rev0/platform.go
new file mode 100644
index 0000000..fed2d71
--- /dev/null
+++ b/hardware/rev0/platform.go
@@ -0,0 +1,177 @@
+package rev0
+
+import (
+ "context"
+
+ "github.com/awonak/EuroPiGo/hardware/common"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+// Pi will be configured during `init()` from platform-specific files.
+// See `hardware/pico/pico.go` and `hardware/nonpico/nonpico.go` for more information.
+var Pi *EuroPiPrototype
+
+var (
+ // static check
+ _ hal.Hardware = (*EuroPiPrototype)(nil)
+)
+
+type EuroPiPrototype struct {
+ common.ContextPi
+
+ // B1 is the Button 1 input on a EuroPi Prototype
+ B1 hal.ButtonInput
+ // B2 is the Button 2 input on a EuroPi Prototype
+ B2 hal.ButtonInput
+ // K1 is the Knob 1 input on a EuroPi Prototype
+ K1 hal.KnobInput
+ // K2 is the Knob 2 input on a EuroPi Prototype
+ K2 hal.KnobInput
+ // AJ1 is the analog voltage output 1 jack on a EuroPi Prototype. It supports a range of output voltages between 0.0 and 3.3 V.
+ AJ1 hal.VoltageOutput
+ // AJ2 is the analog voltage output 2 jack on a EuroPi Prototype. It supports a range of output voltages between 0.0 and 3.3 V.
+ AJ2 hal.VoltageOutput
+ // AJ3 is the analog voltage output 3 jack on a EuroPi Prototype. It supports a range of output voltages between 0.0 and 3.3 V.
+ AJ3 hal.VoltageOutput
+ // AJ4 is the analog voltage output 4 jack on a EuroPi Prototype. It supports a range of output voltages between 0.0 and 3.3 V.
+ AJ4 hal.VoltageOutput
+ // DJ1 is the digital voltage output 1 jack on a EuroPi Prototype. It supports output voltages of 0.0 and 3.3 V.
+ DJ1 hal.VoltageOutput
+ // DJ2 is the digital voltage output 2 jack on a EuroPi Prototype. It supports output voltages of 0.0 and 3.3 V.
+ DJ2 hal.VoltageOutput
+ // DJ3 is the digital voltage output 3 jack on a EuroPi Prototype. It supports output voltages of 0.0 and 3.3 V.
+ DJ3 hal.VoltageOutput
+ // DJ4 is the digital voltage output 4 jack on a EuroPi Prototype. It supports output voltages of 0.0 and 3.3 V.
+ DJ4 hal.VoltageOutput
+ // RND is the random number generator within the EuroPi Prototype.
+ RND hal.RandomGenerator
+}
+
+func (e *EuroPiPrototype) Context() context.Context {
+ return e
+}
+
+func (e *EuroPiPrototype) Revision() hal.Revision {
+ return hal.Revision0
+}
+
+func (e *EuroPiPrototype) Random() hal.RandomGenerator {
+ return e.RND
+}
+
+func (e *EuroPiPrototype) String() string {
+ return "EuroPi Prototype"
+}
+
+func (e *EuroPiPrototype) AJ() [4]hal.VoltageOutput {
+ return [4]hal.VoltageOutput{e.AJ1, e.AJ2, e.AJ3, e.AJ4}
+}
+
+func (e *EuroPiPrototype) DJ() [4]hal.VoltageOutput {
+ return [4]hal.VoltageOutput{e.DJ1, e.DJ2, e.DJ3, e.DJ4}
+}
+
+func (e *EuroPiPrototype) Button(idx int) hal.ButtonInput {
+ switch idx {
+ case 0:
+ return e.B1
+ case 1:
+ return e.B2
+ default:
+ return nil
+ }
+}
+
+func (e *EuroPiPrototype) Knob(idx int) hal.KnobInput {
+ switch idx {
+ case 0:
+ return e.K1
+ case 1:
+ return e.K2
+ default:
+ return nil
+ }
+}
+
+// GetHardware returns a EuroPi hardware device based on hardware `id`.
+// a `nil` result means that the hardware was not found or some sort of error occurred.
+func GetHardware[T any](hw hal.HardwareId) T {
+ var t T
+ if Pi == nil {
+ return t
+ }
+
+ switch hw {
+ case HardwareIdButton1Input:
+ t, _ = Pi.B1.(T)
+ case HardwareIdButton2Input:
+ t, _ = Pi.B2.(T)
+ case HardwareIdKnob1Input:
+ t, _ = Pi.K1.(T)
+ case HardwareIdKnob2Input:
+ t, _ = Pi.K2.(T)
+ case HardwareIdAnalog1Output:
+ t, _ = Pi.AJ1.(T)
+ case HardwareIdAnalog2Output:
+ t, _ = Pi.AJ2.(T)
+ case HardwareIdAnalog3Output:
+ t, _ = Pi.AJ3.(T)
+ case HardwareIdAnalog4Output:
+ t, _ = Pi.AJ4.(T)
+ case HardwareIdDigital1Output:
+ t, _ = Pi.DJ1.(T)
+ case HardwareIdDigital2Output:
+ t, _ = Pi.DJ2.(T)
+ case HardwareIdDigital3Output:
+ t, _ = Pi.DJ3.(T)
+ case HardwareIdDigital4Output:
+ t, _ = Pi.DJ4.(T)
+ case HardwareIdRandom1Generator:
+ t, _ = Pi.RND.(T)
+ default:
+ }
+ return t
+}
+
+// Initialize sets up the hardware
+//
+// This is only to be called by the automatic platform initialization functions
+func Initialize(params InitializationParameters) {
+ Pi = &EuroPiPrototype{
+ ContextPi: common.ContextPi{
+ Context: context.Background(),
+ },
+ B1: common.NewDigitalInput(params.InputButton1),
+ B2: common.NewDigitalInput(params.InputButton2),
+ K1: common.NewAnalogInput(params.InputKnob1, aiInitialConfig),
+ K2: common.NewAnalogInput(params.InputKnob2, aiInitialConfig),
+ AJ1: common.NewVoltageOuput(params.OutputAnalog1, cvInitialConfig),
+ AJ2: common.NewVoltageOuput(params.OutputAnalog2, cvInitialConfig),
+ AJ3: common.NewVoltageOuput(params.OutputAnalog3, cvInitialConfig),
+ AJ4: common.NewVoltageOuput(params.OutputAnalog4, cvInitialConfig),
+ DJ1: common.NewVoltageOuput(params.OutputDigital1, cvInitialConfig),
+ DJ2: common.NewVoltageOuput(params.OutputDigital2, cvInitialConfig),
+ DJ3: common.NewVoltageOuput(params.OutputDigital3, cvInitialConfig),
+ DJ4: common.NewVoltageOuput(params.OutputDigital4, cvInitialConfig),
+ RND: common.NewRandomGenerator(params.DeviceRandomGenerator1),
+ }
+}
+
+// InitializationParameters is a ferry for hardware functions to the interface layer found here
+//
+// This is only to be used by the automatic platform initialization functions
+type InitializationParameters struct {
+ InputButton1 common.DigitalReaderProvider
+ InputButton2 common.DigitalReaderProvider
+ InputKnob1 common.ADCProvider
+ InputKnob2 common.ADCProvider
+ OutputAnalog1 common.PWMProvider
+ OutputAnalog2 common.PWMProvider
+ OutputAnalog3 common.PWMProvider
+ OutputAnalog4 common.PWMProvider
+ OutputDigital1 common.PWMProvider
+ OutputDigital2 common.PWMProvider
+ OutputDigital3 common.PWMProvider
+ OutputDigital4 common.PWMProvider
+ DeviceRandomGenerator1 common.RNDProvider
+}
diff --git a/hardware/rev0/voltageoutput.go b/hardware/rev0/voltageoutput.go
new file mode 100644
index 0000000..b3ff4c8
--- /dev/null
+++ b/hardware/rev0/voltageoutput.go
@@ -0,0 +1,33 @@
+package rev0
+
+import (
+ "time"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+const (
+ // Manually calibrated to best match expected voltages. Additional info:
+ // https://github.com/Allen-Synthesis/EuroPi/blob/main/software/programming_instructions.md#calibrate-the-module
+ CalibratedOffset = 0
+ // The default pwmGroup Top of MaxUint16 caused noisy output. Dropping this down to a 8bit value resulted in much smoother cv output.
+ CalibratedTop = 0xff - CalibratedOffset
+
+ MaxOutputVoltage = 3.3
+ MinOutputVoltage = 0.0
+
+ // We need a rather high frequency to achieve a stable cv ouput, which means we need a rather low duty cycle period.
+ // Set a period of 500ns.
+ DefaultPWMPeriod time.Duration = time.Nanosecond * 500
+)
+
+var (
+ DefaultVoltageOutputCalibration = lerp.NewRemap32[float32, uint16](MinOutputVoltage, MaxOutputVoltage, CalibratedOffset, CalibratedTop)
+
+ cvInitialConfig = hal.VoltageOutputConfig{
+ Period: DefaultPWMPeriod,
+ Monopolar: true,
+ Calibration: DefaultVoltageOutputCalibration,
+ }
+)
diff --git a/hardware/rev1/README.md b/hardware/rev1/README.md
new file mode 100644
index 0000000..44a8a55
--- /dev/null
+++ b/hardware/rev1/README.md
@@ -0,0 +1,22 @@
+# hardware/rev1
+
+This package is used for the [Original EuroPi hardware](https://github.com/Allen-Synthesis/EuroPi/tree/main/hardware).
+
+## Hardware Mapping
+
+| Name | Interface | HardwareId | HardwareId Alias |
+|----|----|----|----|
+| `InputDigital1` | `hal.DigitalInput` | `HardwareIdDigital1Input` | |
+| `InputAnalog1` | `hal.AnalogInput` | `HardwareIdAnalog1Input` | `HardwareIdAnalogue1Input` |
+| `OutputDisplay1` | `hal.DisplayOutput` | `HardwareIdDisplay1Output` | |
+| `InputButton1` | `hal.ButtonInput` | `HardwareIdButton1Input` | |
+| `InputButton2` | `hal.ButtonInput` | `HardwareIdButton2Input` | |
+| `InputKnob1` | `hal.KnobInput` | `HardwareIdKnob1Input` | |
+| `InputKnob2` | `hal.KnobInput` | `HardwareIdKnob2Input` | |
+| `OutputVoltage1` | `hal.VoltageOutput` | `HardwareIdVoltage1Output` | `HardwareIdCV1Output` |
+| `OutputVoltage2` | `hal.VoltageOutput` | `HardwareIdVoltage2Output` | `HardwareIdCV2Output` |
+| `OutputVoltage3` | `hal.VoltageOutput` | `HardwareIdVoltage3Output` | `HardwareIdCV3Output` |
+| `OutputVoltage4` | `hal.VoltageOutput` | `HardwareIdVoltage4Output` | `HardwareIdCV4Output` |
+| `OutputVoltage5` | `hal.VoltageOutput` | `HardwareIdVoltage5Output` | `HardwareIdCV5Output` |
+| `OutputVoltage6` | `hal.VoltageOutput` | `HardwareIdVoltage6Output` | `HardwareIdCV6Output` |
+| `DeviceRandomGenerator1` | `hal.RandomGenerator` | `HardwareIdRandom1Generator` | |
diff --git a/hardware/rev1/analoginput.go b/hardware/rev1/analoginput.go
new file mode 100644
index 0000000..f78ccf0
--- /dev/null
+++ b/hardware/rev1/analoginput.go
@@ -0,0 +1,27 @@
+package rev1
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+const (
+ // DefaultCalibrated[Min|Max]AI was calculated using the EuroPi calibration program:
+ // https://github.com/Allen-Synthesis/EuroPi/blob/main/software/programming_instructions.md#calibrate-the-module
+ DefaultCalibratedMinAI = 300
+ DefaultCalibratedMaxAI = 44009
+
+ DefaultSamples = 1000
+
+ MaxInputVoltage = 10.0
+ MinInputVoltage = 0.0
+)
+
+var (
+ DefaultAICalibration = lerp.NewRemap32[uint16, float32](DefaultCalibratedMinAI, DefaultCalibratedMaxAI, MinInputVoltage, MaxInputVoltage)
+
+ aiInitialConfig = hal.AnalogInputConfig{
+ Samples: DefaultSamples,
+ Calibration: DefaultAICalibration,
+ }
+)
diff --git a/hardware/rev1/hardware.go b/hardware/rev1/hardware.go
new file mode 100644
index 0000000..a82c057
--- /dev/null
+++ b/hardware/rev1/hardware.go
@@ -0,0 +1,41 @@
+package rev1
+
+import "github.com/awonak/EuroPiGo/hardware/hal"
+
+// aliases for module revision specific referencing
+const (
+ // DI: Digital Input
+ HardwareIdDigital1Input = hal.HardwareIdDigital1Input
+ // AI: Analog(ue) Input
+ HardwareIdAnalog1Input = hal.HardwareIdAnalog1Input
+ // Display
+ HardwareIdDisplay1Output = hal.HardwareIdDisplay1Output
+ // K1
+ HardwareIdKnob1Input = hal.HardwareIdKnob1Input
+ // K2
+ HardwareIdKnob2Input = hal.HardwareIdKnob2Input
+ // B1
+ HardwareIdButton1Input = hal.HardwareIdButton1Input
+ // B2
+ HardwareIdButton2Input = hal.HardwareIdButton2Input
+ // CV1
+ HardwareIdCV1Output = hal.HardwareIdVoltage1Output
+ // CV2
+ HardwareIdCV2Output = hal.HardwareIdVoltage2Output
+ // CV3
+ HardwareIdCV3Output = hal.HardwareIdVoltage3Output
+ // CV4
+ HardwareIdCV4Output = hal.HardwareIdVoltage4Output
+ // CV5
+ HardwareIdCV5Output = hal.HardwareIdVoltage5Output
+ // CV6
+ HardwareIdCV6Output = hal.HardwareIdVoltage6Output
+ // RNG
+ HardwareIdRandom1Generator = hal.HardwareIdRandom1Generator
+)
+
+// aliases for friendly internationali(s|z)ation, colloquialisms, and naming conventions
+const (
+ HardwareIdAnalogue1Input = HardwareIdAnalog1Input
+ HardwareIdOLED1Output = HardwareIdDisplay1Output
+)
diff --git a/hardware/rev1/platform.go b/hardware/rev1/platform.go
new file mode 100644
index 0000000..0b679c7
--- /dev/null
+++ b/hardware/rev1/platform.go
@@ -0,0 +1,174 @@
+package rev1
+
+import (
+ "context"
+
+ "github.com/awonak/EuroPiGo/hardware/common"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+// Pi will be configured during `init()` from platform-specific files.
+// See `hardware/pico/pico.go` and `hardware/nonpico/nonpico.go` for more information.
+var Pi *EuroPi
+
+type EuroPi struct {
+ common.ContextPi
+
+ // DI is the Digital Input on a EuroPi
+ DI hal.DigitalInput
+ // AI is the Analogue Input on a EuroPi
+ AI hal.AnalogInput
+ // OLED is the display output on a EuroPi
+ OLED hal.DisplayOutput
+ // B1 is the Button 1 input on a EuroPi
+ B1 hal.ButtonInput
+ // B2 is the Button 2 input on a EuroPi
+ B2 hal.ButtonInput
+ // K1 is the Knob 1 input on a EuroPi
+ K1 hal.KnobInput
+ // K2 is the Knob 2 input on a EuroPi
+ K2 hal.KnobInput
+ // CV1 is the voltage output 1 jack on a EuroPi. It supports a range of output voltages between 0.0 and 10.0 V.
+ CV1 hal.VoltageOutput
+ // CV2 is the voltage output 2 jack on a EuroPi. It supports a range of output voltages between 0.0 and 10.0 V.
+ CV2 hal.VoltageOutput
+ // CV3 is the voltage output 3 jack on a EuroPi. It supports a range of output voltages between 0.0 and 10.0 V.
+ CV3 hal.VoltageOutput
+ // CV4 is the voltage output 4 jack on a EuroPi. It supports a range of output voltages between 0.0 and 10.0 V.
+ CV4 hal.VoltageOutput
+ // CV5 is the voltage output 5 jack on a EuroPi. It supports a range of output voltages between 0.0 and 10.0 V.
+ CV5 hal.VoltageOutput
+ // CV6 is the voltage output 6 jack on a EuroPi. It supports a range of output voltages between 0.0 and 10.0 V.
+ CV6 hal.VoltageOutput
+ // RND is the random number generator within the EuroPi
+ RND hal.RandomGenerator
+}
+
+func (e *EuroPi) Context() context.Context {
+ return e
+}
+
+func (e *EuroPi) Revision() hal.Revision {
+ return hal.Revision1
+}
+
+func (e *EuroPi) Random() hal.RandomGenerator {
+ return e.RND
+}
+
+func (e *EuroPi) String() string {
+ return "EuroPi"
+}
+
+func (e *EuroPi) CV() [6]hal.VoltageOutput {
+ return [6]hal.VoltageOutput{e.CV1, e.CV2, e.CV3, e.CV4, e.CV5, e.CV6}
+}
+
+func (e *EuroPi) Button(idx int) hal.ButtonInput {
+ switch idx {
+ case 0:
+ return e.B1
+ case 1:
+ return e.B2
+ default:
+ return nil
+ }
+}
+
+func (e *EuroPi) Knob(idx int) hal.KnobInput {
+ switch idx {
+ case 0:
+ return e.K1
+ case 1:
+ return e.K2
+ default:
+ return nil
+ }
+}
+
+// GetHardware returns a EuroPi hardware device based on hardware `id`.
+// a `nil` result means that the hardware was not found or some sort of error occurred.
+func GetHardware[T any](hw hal.HardwareId) T {
+ var t T
+ if Pi == nil {
+ return t
+ }
+
+ switch hw {
+ case HardwareIdDigital1Input:
+ t, _ = Pi.DI.(T)
+ case HardwareIdAnalog1Input:
+ t, _ = Pi.AI.(T)
+ case HardwareIdDisplay1Output:
+ t, _ = Pi.OLED.(T)
+ case HardwareIdButton1Input:
+ t, _ = Pi.B1.(T)
+ case HardwareIdButton2Input:
+ t, _ = Pi.B2.(T)
+ case HardwareIdKnob1Input:
+ t, _ = Pi.K1.(T)
+ case HardwareIdKnob2Input:
+ t, _ = Pi.K2.(T)
+ case HardwareIdCV1Output:
+ t, _ = Pi.CV1.(T)
+ case HardwareIdCV2Output:
+ t, _ = Pi.CV2.(T)
+ case HardwareIdCV3Output:
+ t, _ = Pi.CV3.(T)
+ case HardwareIdCV4Output:
+ t, _ = Pi.CV4.(T)
+ case HardwareIdCV5Output:
+ t, _ = Pi.CV5.(T)
+ case HardwareIdCV6Output:
+ t, _ = Pi.CV6.(T)
+ case HardwareIdRandom1Generator:
+ t, _ = Pi.RND.(T)
+ default:
+ }
+ return t
+}
+
+// Initialize sets up the hardware
+//
+// This is only to be called by the automatic platform initialization functions
+func Initialize(params InitializationParameters) {
+ Pi = &EuroPi{
+ ContextPi: common.ContextPi{
+ Context: context.Background(),
+ },
+ DI: common.NewDigitalInput(params.InputDigital1),
+ AI: common.NewAnalogInput(params.InputAnalog1, aiInitialConfig),
+ OLED: common.NewDisplayOutput(params.OutputDisplay1),
+ B1: common.NewDigitalInput(params.InputButton1),
+ B2: common.NewDigitalInput(params.InputButton2),
+ K1: common.NewAnalogInput(params.InputKnob1, aiInitialConfig),
+ K2: common.NewAnalogInput(params.InputKnob2, aiInitialConfig),
+ CV1: common.NewVoltageOuput(params.OutputVoltage1, cvInitialConfig),
+ CV2: common.NewVoltageOuput(params.OutputVoltage2, cvInitialConfig),
+ CV3: common.NewVoltageOuput(params.OutputVoltage3, cvInitialConfig),
+ CV4: common.NewVoltageOuput(params.OutputVoltage4, cvInitialConfig),
+ CV5: common.NewVoltageOuput(params.OutputVoltage5, cvInitialConfig),
+ CV6: common.NewVoltageOuput(params.OutputVoltage6, cvInitialConfig),
+ RND: common.NewRandomGenerator(params.DeviceRandomGenerator1),
+ }
+}
+
+// InitializationParameters is a ferry for hardware functions to the interface layer found here
+//
+// This is only to be used by the automatic platform initialization functions
+type InitializationParameters struct {
+ InputDigital1 common.DigitalReaderProvider
+ InputAnalog1 common.ADCProvider
+ OutputDisplay1 common.DisplayProvider
+ InputButton1 common.DigitalReaderProvider
+ InputButton2 common.DigitalReaderProvider
+ InputKnob1 common.ADCProvider
+ InputKnob2 common.ADCProvider
+ OutputVoltage1 common.PWMProvider
+ OutputVoltage2 common.PWMProvider
+ OutputVoltage3 common.PWMProvider
+ OutputVoltage4 common.PWMProvider
+ OutputVoltage5 common.PWMProvider
+ OutputVoltage6 common.PWMProvider
+ DeviceRandomGenerator1 common.RNDProvider
+}
diff --git a/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go
new file mode 100644
index 0000000..5b2c14f
--- /dev/null
+++ b/hardware/rev1/voltageoutput.go
@@ -0,0 +1,33 @@
+package rev1
+
+import (
+ "time"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+const (
+ // Manually calibrated to best match expected voltages. Additional info:
+ // https://github.com/Allen-Synthesis/EuroPi/blob/main/software/programming_instructions.md#calibrate-the-module
+ CalibratedOffset = 0
+ // The default pwmGroup Top of MaxUint16 caused noisy output. Dropping this down to a 8bit value resulted in much smoother cv output.
+ CalibratedTop = 0xff - CalibratedOffset
+
+ MaxOutputVoltage = 10.0
+ MinOutputVoltage = 0.0
+
+ // We need a rather high frequency to achieve a stable cv ouput, which means we need a rather low duty cycle period.
+ // Set a period of 500ns.
+ DefaultPWMPeriod time.Duration = time.Nanosecond * 500
+)
+
+var (
+ DefaultVoltageOutputCalibration = lerp.NewRemap32[float32, uint16](MinOutputVoltage, MaxOutputVoltage, CalibratedOffset, CalibratedTop)
+
+ cvInitialConfig = hal.VoltageOutputConfig{
+ Period: DefaultPWMPeriod,
+ Monopolar: true,
+ Calibration: DefaultVoltageOutputCalibration,
+ }
+)
diff --git a/hardware/revisiondetection.go b/hardware/revisiondetection.go
new file mode 100644
index 0000000..6d55ffd
--- /dev/null
+++ b/hardware/revisiondetection.go
@@ -0,0 +1,59 @@
+package hardware
+
+import (
+ "sync"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+// GetRevision returns the currently detected hardware revision.
+// If the current revision hasn't been detected, yet, then this call
+// will block until it is.
+func GetRevision() hal.Revision {
+ var waitForDetect sync.WaitGroup
+ waitForDetect.Add(1)
+ var detectedRevision hal.Revision
+ OnRevisionDetected(func(revision hal.Revision) {
+ detectedRevision = revision
+ waitForDetect.Done()
+ })
+ waitForDetect.Wait()
+ return detectedRevision
+}
+
+var (
+ onRevisionDetected chan func(revision hal.Revision)
+ revisionChannelInit sync.Once
+ revisionWgDone sync.Once
+)
+
+func ensureOnRevisionDetection() {
+ revisionChannelInit.Do(func() {
+ onRevisionDetected = make(chan func(revision hal.Revision), 10)
+ })
+}
+
+func OnRevisionDetected(fn func(revision hal.Revision)) {
+ if fn == nil {
+ return
+ }
+ ensureOnRevisionDetection()
+ onRevisionDetected <- fn
+}
+
+// SetDetectedRevision sets the currently detected hardware revision.
+// This should not be called directly.
+func SetDetectedRevision(opts ...hal.Revision) {
+ ensureOnRevisionDetection()
+ // need to be sure it's ready before we can done() it
+ hal.RevisionMark = hal.NewRevisionMark(opts...)
+ revisionWgDone.Do(func() {
+ go func() {
+ for fn := range onRevisionDetected {
+ if fn != nil {
+ fn(hal.RevisionMark.Revision())
+ }
+ }
+ }()
+ })
+}
diff --git a/internal/nonpico/common/adc.go b/internal/nonpico/common/adc.go
new file mode 100644
index 0000000..d409347
--- /dev/null
+++ b/internal/nonpico/common/adc.go
@@ -0,0 +1,40 @@
+//go:build !pico
+// +build !pico
+
+package common
+
+import (
+ "fmt"
+
+ "github.com/awonak/EuroPiGo/event"
+ "github.com/awonak/EuroPiGo/hardware/common"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+type nonPicoAdc struct {
+ id hal.HardwareId
+ value uint16
+}
+
+var (
+ // static check
+ _ common.ADCProvider = (*nonPicoAdc)(nil)
+)
+
+func NewNonPicoAdc(id hal.HardwareId) *nonPicoAdc {
+ adc := &nonPicoAdc{
+ id: id,
+ }
+ event.Subscribe(bus, fmt.Sprintf("hw_value_%d", id), func(msg HwMessageADCValue) {
+ adc.value = msg.Value
+ })
+ return adc
+}
+
+func (a *nonPicoAdc) Get(samples int) uint16 {
+ var sum int
+ for i := 0; i < samples; i++ {
+ sum += int(a.value)
+ }
+ return uint16(sum / samples)
+}
diff --git a/internal/nonpico/common/bus.go b/internal/nonpico/common/bus.go
new file mode 100644
index 0000000..3b59270
--- /dev/null
+++ b/internal/nonpico/common/bus.go
@@ -0,0 +1,45 @@
+//go:build !pico
+// +build !pico
+
+package common
+
+import (
+ "fmt"
+
+ "github.com/awonak/EuroPiGo/event"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+var (
+ bus = event.NewBus()
+)
+
+func SetDigitalValue(hid hal.HardwareId, value bool) {
+ bus.Post(fmt.Sprintf("hw_value_%d", hid), HwMessageDigitalValue{
+ Value: value,
+ })
+}
+
+func TriggerInterrupt(hid hal.HardwareId, change hal.ChangeFlags) {
+ bus.Post(fmt.Sprintf("hw_interrupt_%d", hid), HwMessageInterrupt{
+ Change: change,
+ })
+}
+
+func SetADCValue(hid hal.HardwareId, value uint16) {
+ bus.Post(fmt.Sprintf("hw_value_%d", hid), HwMessageADCValue{
+ Value: value,
+ })
+}
+
+func OnPWMValue(hid hal.HardwareId, fn func(hid hal.HardwareId, value uint16, voltage float32)) {
+ event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", hid), func(msg HwMessagePwmValue) {
+ fn(hid, msg.Value, msg.Voltage)
+ })
+}
+
+func OnDisplayOutput(hid hal.HardwareId, fn func(hid hal.HardwareId, op HwDisplayOp, params []int16)) {
+ event.Subscribe(bus, fmt.Sprintf("hw_display_%d", hid), func(msg HwMessageDisplay) {
+ fn(hid, msg.Op, msg.Operands)
+ })
+}
diff --git a/internal/nonpico/common/digitalreader.go b/internal/nonpico/common/digitalreader.go
new file mode 100644
index 0000000..6892807
--- /dev/null
+++ b/internal/nonpico/common/digitalreader.go
@@ -0,0 +1,46 @@
+//go:build !pico
+// +build !pico
+
+package common
+
+import (
+ "fmt"
+
+ "github.com/awonak/EuroPiGo/event"
+ "github.com/awonak/EuroPiGo/hardware/common"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+type nonPicoDigitalReader struct {
+ id hal.HardwareId
+ value bool
+}
+
+var (
+ // static check
+ _ common.DigitalReaderProvider = (*nonPicoDigitalReader)(nil)
+)
+
+func NewNonPicoDigitalReader(id hal.HardwareId) *nonPicoDigitalReader {
+ dr := &nonPicoDigitalReader{
+ id: id,
+ value: true, // start off in high, as that's actually read as low
+ }
+ event.Subscribe(bus, fmt.Sprintf("hw_value_%d", id), func(msg HwMessageDigitalValue) {
+ dr.value = !msg.Value
+ })
+ return dr
+}
+
+func (d *nonPicoDigitalReader) Get() bool {
+ // Invert signal to match expected behavior.
+ return !d.value
+}
+
+func (d *nonPicoDigitalReader) SetHandler(changes hal.ChangeFlags, handler func()) {
+ event.Subscribe(bus, fmt.Sprintf("hw_interrupt_%d", d.id), func(msg HwMessageInterrupt) {
+ if (msg.Change & changes) != 0 {
+ handler()
+ }
+ })
+}
diff --git a/internal/nonpico/common/displaymode.go b/internal/nonpico/common/displaymode.go
new file mode 100644
index 0000000..13ab116
--- /dev/null
+++ b/internal/nonpico/common/displaymode.go
@@ -0,0 +1,11 @@
+//go:build !pico
+// +build !pico
+
+package common
+
+type DisplayMode int
+
+const (
+ DisplayModeSeparate = DisplayMode(iota)
+ DisplayModeCombined
+)
diff --git a/internal/nonpico/common/displayoutput.go b/internal/nonpico/common/displayoutput.go
new file mode 100644
index 0000000..a55a418
--- /dev/null
+++ b/internal/nonpico/common/displayoutput.go
@@ -0,0 +1,61 @@
+//go:build !pico
+// +build !pico
+
+package common
+
+import (
+ "fmt"
+ "image/color"
+
+ "github.com/awonak/EuroPiGo/hardware/common"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+const (
+ oledWidth = 128
+ oledHeight = 32
+)
+
+type nonPicoDisplayOutput struct {
+ id hal.HardwareId
+ width int16
+ height int16
+}
+
+var (
+ // static check
+ _ common.DisplayProvider = (*nonPicoDisplayOutput)(nil)
+)
+
+func NewNonPicoDisplayOutput(id hal.HardwareId) *nonPicoDisplayOutput {
+ dp := &nonPicoDisplayOutput{
+ id: id,
+ width: oledWidth,
+ height: oledHeight,
+ }
+
+ return dp
+}
+
+func (d *nonPicoDisplayOutput) ClearBuffer() {
+ bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{
+ Op: HwDisplayOpClearBuffer,
+ })
+}
+
+func (d *nonPicoDisplayOutput) Size() (x, y int16) {
+ return d.width, d.height
+}
+func (d *nonPicoDisplayOutput) SetPixel(x, y int16, c color.RGBA) {
+ bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{
+ Op: HwDisplayOpSetPixel,
+ Operands: []int16{x, y, int16(c.R), int16(c.B), int16(c.G), int16(c.A)},
+ })
+}
+
+func (d *nonPicoDisplayOutput) Display() error {
+ bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{
+ Op: HwDisplayOpDisplay,
+ })
+ return nil
+}
diff --git a/internal/nonpico/common/messages.go b/internal/nonpico/common/messages.go
new file mode 100644
index 0000000..5780de4
--- /dev/null
+++ b/internal/nonpico/common/messages.go
@@ -0,0 +1,39 @@
+package common
+
+import "github.com/awonak/EuroPiGo/hardware/hal"
+
+// HwMessageDigitalValue represents a digital value update
+type HwMessageDigitalValue struct {
+ Value bool
+}
+
+// HwMessageADCValue represents an ADC value update
+type HwMessageADCValue struct {
+ Value uint16
+}
+
+// HwMessageInterrupt represents an interrupt
+type HwMessageInterrupt struct {
+ Change hal.ChangeFlags
+}
+
+// HwMessagePwmValue represents a pulse width modulator value update
+type HwMessagePwmValue struct {
+ Value uint16
+ Voltage float32
+}
+
+// HwMessageDisplay represents a display update.
+type HwMessageDisplay struct {
+ Op HwDisplayOp
+ Operands []int16
+}
+
+// HwDisplayOp is the operation for a display update.
+type HwDisplayOp int
+
+const (
+ HwDisplayOpClearBuffer = HwDisplayOp(iota)
+ HwDisplayOpSetPixel
+ HwDisplayOpDisplay
+)
diff --git a/internal/nonpico/common/pwm.go b/internal/nonpico/common/pwm.go
new file mode 100644
index 0000000..073b704
--- /dev/null
+++ b/internal/nonpico/common/pwm.go
@@ -0,0 +1,56 @@
+//go:build !pico
+// +build !pico
+
+package common
+
+import (
+ "fmt"
+
+ "github.com/awonak/EuroPiGo/hardware/common"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+type nonPicoPwm struct {
+ id hal.HardwareId
+ cal lerp.Remapper32[float32, uint16]
+ v float32
+}
+
+var (
+ // static check
+ _ common.PWMProvider = (*nonPicoPwm)(nil)
+)
+
+func NewNonPicoPwm(id hal.HardwareId, cal lerp.Remapper32[float32, uint16]) *nonPicoPwm {
+ p := &nonPicoPwm{
+ id: id,
+ cal: cal,
+ }
+ return p
+}
+
+func (p *nonPicoPwm) Configure(config hal.VoltageOutputConfig) error {
+ return nil
+}
+
+func (p *nonPicoPwm) Set(v float32) {
+ pulseWidth := p.cal.Remap(v)
+ p.v = p.cal.Unmap(pulseWidth)
+ bus.Post(fmt.Sprintf("hw_pwm_%d", p.id), HwMessagePwmValue{
+ Value: pulseWidth,
+ Voltage: p.v,
+ })
+}
+
+func (p *nonPicoPwm) Get() float32 {
+ return p.v
+}
+
+func (p *nonPicoPwm) MinVoltage() float32 {
+ return p.cal.InputMinimum()
+}
+
+func (p *nonPicoPwm) MaxVoltage() float32 {
+ return p.cal.InputMaximum()
+}
diff --git a/internal/nonpico/nonpico.go b/internal/nonpico/nonpico.go
new file mode 100644
index 0000000..87e6ad2
--- /dev/null
+++ b/internal/nonpico/nonpico.go
@@ -0,0 +1,7 @@
+package nonpico
+
+// This file is required for compilation to occur. Do not remove it
+// or the empty init function at the bottom of the file.
+
+func init() {
+}
diff --git a/internal/nonpico/platform.go b/internal/nonpico/platform.go
new file mode 100644
index 0000000..b01d3b5
--- /dev/null
+++ b/internal/nonpico/platform.go
@@ -0,0 +1,38 @@
+//go:build !pico
+// +build !pico
+
+package nonpico
+
+import (
+ "github.com/awonak/EuroPiGo/hardware"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/internal/nonpico/rev0"
+ "github.com/awonak/EuroPiGo/internal/nonpico/rev1"
+)
+
+func initRevision0() {
+ rev0.DoInit()
+}
+
+func initRevision1() {
+ rev1.DoInit()
+}
+
+func initRevision2() {
+ //TODO: rev2.DoInit()
+}
+
+func init() {
+ hardware.OnRevisionDetected(func(revision hal.Revision) {
+ switch revision {
+ case hal.Revision0:
+ initRevision0()
+ case hal.Revision1:
+ initRevision1()
+ case hal.Revision2:
+ initRevision2()
+ default:
+ }
+ hardware.SetReady()
+ })
+}
diff --git a/internal/nonpico/rev0/api.go b/internal/nonpico/rev0/api.go
new file mode 100644
index 0000000..5d6a38e
--- /dev/null
+++ b/internal/nonpico/rev0/api.go
@@ -0,0 +1,26 @@
+//go:build !pico
+// +build !pico
+
+package rev0
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+type voltageOutputMsg struct {
+ Kind string `json:"kind"`
+ HardwareId hal.HardwareId `json:"hardwareId"`
+ Voltage float32 `json:"voltage"`
+}
+
+type setDigitalInputMsg struct {
+ Kind string `json:"kind"`
+ HardwareId hal.HardwareId `json:"hardwareId"`
+ Value bool `json:"value"`
+}
+
+type setAnalogInputMsg struct {
+ Kind string `json:"kind"`
+ HardwareId hal.HardwareId `json:"hardwareId"`
+ Voltage float32 `json:"voltage"`
+}
diff --git a/internal/nonpico/rev0/listeners.go b/internal/nonpico/rev0/listeners.go
new file mode 100644
index 0000000..88a6516
--- /dev/null
+++ b/internal/nonpico/rev0/listeners.go
@@ -0,0 +1,68 @@
+//go:build !pico
+// +build !pico
+
+package rev0
+
+import (
+ "sync"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/hardware/rev0"
+ "github.com/awonak/EuroPiGo/internal/nonpico/common"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+func setupDefaultState() {
+ common.SetDigitalValue(rev0.HardwareIdButton1Input, false)
+ common.SetDigitalValue(rev0.HardwareIdButton2Input, false)
+
+ common.SetADCValue(rev0.HardwareIdKnob1Input, aiLerp.Lerp(0.5))
+ common.SetADCValue(rev0.HardwareIdKnob2Input, aiLerp.Lerp(0.5))
+}
+
+func setupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) {
+ ids := []hal.HardwareId{
+ rev0.HardwareIdAnalog1Output,
+ rev0.HardwareIdAnalog2Output,
+ rev0.HardwareIdAnalog3Output,
+ rev0.HardwareIdAnalog4Output,
+ rev0.HardwareIdDigital1Output,
+ rev0.HardwareIdDigital2Output,
+ rev0.HardwareIdDigital3Output,
+ rev0.HardwareIdDigital4Output,
+ }
+ for _, id := range ids {
+ common.OnPWMValue(id, func(hid hal.HardwareId, value uint16, voltage float32) {
+ cb(hid, voltage)
+ })
+ }
+}
+
+var (
+ states sync.Map
+)
+
+func setDigitalInput(id hal.HardwareId, value bool) {
+ prevState, _ := states.Load(id)
+
+ states.Store(id, value)
+ common.SetDigitalValue(id, value)
+
+ if prevState != value {
+ if value {
+ // rising
+ common.TriggerInterrupt(id, hal.ChangeRising)
+ } else {
+ // falling
+ common.TriggerInterrupt(id, hal.ChangeFalling)
+ }
+ }
+}
+
+var (
+ aiLerp = lerp.NewLerp32[uint16](rev0.DefaultCalibratedMinAI, rev0.DefaultCalibratedMaxAI)
+)
+
+func setAnalogInput(id hal.HardwareId, voltage float32) {
+ common.SetADCValue(id, aiLerp.Lerp(voltage))
+}
diff --git a/internal/nonpico/rev0/platform.go b/internal/nonpico/rev0/platform.go
new file mode 100644
index 0000000..128d060
--- /dev/null
+++ b/internal/nonpico/rev0/platform.go
@@ -0,0 +1,24 @@
+package rev0
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/rev0"
+ "github.com/awonak/EuroPiGo/internal/nonpico/common"
+)
+
+func DoInit() {
+ rev0.Initialize(rev0.InitializationParameters{
+ InputButton1: common.NewNonPicoDigitalReader(rev0.HardwareIdButton1Input),
+ InputButton2: common.NewNonPicoDigitalReader(rev0.HardwareIdButton2Input),
+ InputKnob1: common.NewNonPicoAdc(rev0.HardwareIdKnob1Input),
+ InputKnob2: common.NewNonPicoAdc(rev0.HardwareIdKnob2Input),
+ OutputAnalog1: common.NewNonPicoPwm(rev0.HardwareIdAnalog1Output, rev0.DefaultVoltageOutputCalibration),
+ OutputAnalog2: common.NewNonPicoPwm(rev0.HardwareIdAnalog2Output, rev0.DefaultVoltageOutputCalibration),
+ OutputAnalog3: common.NewNonPicoPwm(rev0.HardwareIdAnalog3Output, rev0.DefaultVoltageOutputCalibration),
+ OutputAnalog4: common.NewNonPicoPwm(rev0.HardwareIdAnalog4Output, rev0.DefaultVoltageOutputCalibration),
+ OutputDigital1: common.NewNonPicoPwm(rev0.HardwareIdDigital1Output, rev0.DefaultVoltageOutputCalibration),
+ OutputDigital2: common.NewNonPicoPwm(rev0.HardwareIdDigital2Output, rev0.DefaultVoltageOutputCalibration),
+ OutputDigital3: common.NewNonPicoPwm(rev0.HardwareIdDigital3Output, rev0.DefaultVoltageOutputCalibration),
+ OutputDigital4: common.NewNonPicoPwm(rev0.HardwareIdDigital4Output, rev0.DefaultVoltageOutputCalibration),
+ DeviceRandomGenerator1: nil,
+ })
+}
diff --git a/internal/nonpico/rev0/site/index.html b/internal/nonpico/rev0/site/index.html
new file mode 100644
index 0000000..e343a54
--- /dev/null
+++ b/internal/nonpico/rev0/site/index.html
@@ -0,0 +1,173 @@
+
+
+
+
+
+ EuroPi Tester
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/nonpico/rev0/wsactivation.go b/internal/nonpico/rev0/wsactivation.go
new file mode 100644
index 0000000..27ec08e
--- /dev/null
+++ b/internal/nonpico/rev0/wsactivation.go
@@ -0,0 +1,129 @@
+//go:build !pico
+// +build !pico
+
+package rev0
+
+import (
+ "context"
+ "embed"
+ "encoding/json"
+ "io/fs"
+ "log"
+ "net/http"
+ _ "net/http/pprof"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/internal/nonpico/ws"
+)
+
+type WSActivation struct {
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+func ActivateWebSocket(ctx context.Context) *WSActivation {
+ a := &WSActivation{}
+
+ a.Start(ctx)
+
+ return a
+}
+
+func (a *WSActivation) Shutdown() error {
+ if a.cancel != nil {
+ a.cancel()
+ }
+ return nil
+}
+
+//go:embed site
+var nonPicoSiteContent embed.FS
+
+func (a *WSActivation) Start(ctx context.Context) {
+ a.ctx, a.cancel = context.WithCancel(ctx)
+
+ // initialize default state
+ setupDefaultState()
+
+ go func() {
+ defer a.cancel()
+
+ subFS, _ := fs.Sub(nonPicoSiteContent, "site")
+ http.Handle("/", http.FileServer(http.FS(subFS)))
+ http.HandleFunc("/ws", a.apiHandler)
+ if err := http.ListenAndServe(":8080", nil); err != nil {
+ panic(err)
+ }
+ }()
+}
+
+func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) {
+ log.Println(r.URL, "rev0.apiHandler")
+
+ if r.Body != nil {
+ // just in case someone sent us a body
+ defer r.Body.Close()
+ }
+
+ sock, err := ws.Upgrade(w, r)
+ if err != nil {
+ log.Println("failed to upgrade websocket connection:", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ defer sock.Close()
+
+ setupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) {
+ _ = sock.WriteJSON(voltageOutputMsg{
+ Kind: "voltageOutput",
+ HardwareId: id,
+ Voltage: voltage,
+ })
+ })
+
+ type kind struct {
+ Kind string `json:"kind"`
+ }
+
+ for {
+ // test for doneness
+ select {
+ case <-sock.Done():
+ break
+ default:
+ }
+
+ blob, err := sock.ReadMessage()
+ if err != nil {
+ break
+ }
+
+ var k kind
+ if err := json.Unmarshal(blob, &k); err != nil {
+ sock.SetError(err)
+ break
+ }
+
+ switch k.Kind {
+ case "setDigitalInput":
+ var di setDigitalInputMsg
+ if err := json.Unmarshal(blob, &di); err != nil {
+ sock.SetError(err)
+ break
+ }
+ setDigitalInput(di.HardwareId, di.Value)
+
+ case "setAnalogInput":
+ var ai setAnalogInputMsg
+ if err := json.Unmarshal(blob, &ai); err != nil {
+ sock.SetError(err)
+ break
+ }
+ setAnalogInput(ai.HardwareId, ai.Voltage)
+
+ default:
+ // ignore
+ }
+ }
+}
diff --git a/internal/nonpico/rev1/api.go b/internal/nonpico/rev1/api.go
new file mode 100644
index 0000000..e652fce
--- /dev/null
+++ b/internal/nonpico/rev1/api.go
@@ -0,0 +1,43 @@
+//go:build !pico
+// +build !pico
+
+package rev1
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/internal/nonpico/common"
+)
+
+type voltageOutputMsg struct {
+ Kind string `json:"kind"`
+ HardwareId hal.HardwareId `json:"hardwareId"`
+ Voltage float32 `json:"voltage"`
+}
+
+// displayMode = displayModeSeparate (0)
+type displayOutputMsg struct {
+ Kind string `json:"kind"`
+ HardwareId hal.HardwareId `json:"hardwareId"`
+ Op common.HwDisplayOp `json:"op"`
+ Params []int16 `json:"params"`
+}
+
+// displayMode = displayModeCombined (1)
+type displayScreenOuptutMsg struct {
+ Kind string `json:"kind"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Data []byte `json:"data"`
+}
+
+type setDigitalInputMsg struct {
+ Kind string `json:"kind"`
+ HardwareId hal.HardwareId `json:"hardwareId"`
+ Value bool `json:"value"`
+}
+
+type setAnalogInputMsg struct {
+ Kind string `json:"kind"`
+ HardwareId hal.HardwareId `json:"hardwareId"`
+ Voltage float32 `json:"voltage"`
+}
diff --git a/internal/nonpico/rev1/listeners.go b/internal/nonpico/rev1/listeners.go
new file mode 100644
index 0000000..cd8a5a7
--- /dev/null
+++ b/internal/nonpico/rev1/listeners.go
@@ -0,0 +1,65 @@
+//go:build !pico
+// +build !pico
+
+package rev1
+
+import (
+ "sync"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/hardware/rev1"
+ "github.com/awonak/EuroPiGo/internal/nonpico/common"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+func setupDefaultState() {
+ common.SetDigitalValue(rev1.HardwareIdDigital1Input, false)
+ common.SetADCValue(rev1.HardwareIdAnalog1Input, rev1.DefaultCalibratedMaxAI)
+
+ common.SetDigitalValue(rev1.HardwareIdButton1Input, false)
+ common.SetDigitalValue(rev1.HardwareIdButton2Input, false)
+
+ common.SetADCValue(rev1.HardwareIdKnob1Input, aiLerp.Lerp(0.5))
+ common.SetADCValue(rev1.HardwareIdKnob2Input, aiLerp.Lerp(0.5))
+}
+
+func setupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) {
+ for id := hal.HardwareIdVoltage1Output; id <= hal.HardwareIdVoltage6Output; id++ {
+ common.OnPWMValue(id, func(hid hal.HardwareId, value uint16, voltage float32) {
+ cb(hid, voltage)
+ })
+ }
+}
+
+func setupDisplayOutputListener(cb func(id hal.HardwareId, op common.HwDisplayOp, params []int16)) {
+ common.OnDisplayOutput(hal.HardwareIdDisplay1Output, cb)
+}
+
+var (
+ states sync.Map
+)
+
+func setDigitalInput(id hal.HardwareId, value bool) {
+ prevState, _ := states.Load(id)
+
+ states.Store(id, value)
+ common.SetDigitalValue(id, value)
+
+ if prevState != value {
+ if value {
+ // rising
+ common.TriggerInterrupt(id, hal.ChangeRising)
+ } else {
+ // falling
+ common.TriggerInterrupt(id, hal.ChangeFalling)
+ }
+ }
+}
+
+var (
+ aiLerp = lerp.NewLerp32[uint16](rev1.DefaultCalibratedMinAI, rev1.DefaultCalibratedMaxAI)
+)
+
+func setAnalogInput(id hal.HardwareId, voltage float32) {
+ common.SetADCValue(id, aiLerp.Lerp(voltage))
+}
diff --git a/internal/nonpico/rev1/platform.go b/internal/nonpico/rev1/platform.go
new file mode 100644
index 0000000..169d266
--- /dev/null
+++ b/internal/nonpico/rev1/platform.go
@@ -0,0 +1,25 @@
+package rev1
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/rev1"
+ "github.com/awonak/EuroPiGo/internal/nonpico/common"
+)
+
+func DoInit() {
+ rev1.Initialize(rev1.InitializationParameters{
+ InputDigital1: common.NewNonPicoDigitalReader(rev1.HardwareIdDigital1Input),
+ InputAnalog1: common.NewNonPicoAdc(rev1.HardwareIdAnalog1Input),
+ OutputDisplay1: common.NewNonPicoDisplayOutput(rev1.HardwareIdDisplay1Output),
+ InputButton1: common.NewNonPicoDigitalReader(rev1.HardwareIdButton1Input),
+ InputButton2: common.NewNonPicoDigitalReader(rev1.HardwareIdButton2Input),
+ InputKnob1: common.NewNonPicoAdc(rev1.HardwareIdKnob1Input),
+ InputKnob2: common.NewNonPicoAdc(rev1.HardwareIdKnob2Input),
+ OutputVoltage1: common.NewNonPicoPwm(rev1.HardwareIdCV1Output, rev1.DefaultVoltageOutputCalibration),
+ OutputVoltage2: common.NewNonPicoPwm(rev1.HardwareIdCV2Output, rev1.DefaultVoltageOutputCalibration),
+ OutputVoltage3: common.NewNonPicoPwm(rev1.HardwareIdCV3Output, rev1.DefaultVoltageOutputCalibration),
+ OutputVoltage4: common.NewNonPicoPwm(rev1.HardwareIdCV4Output, rev1.DefaultVoltageOutputCalibration),
+ OutputVoltage5: common.NewNonPicoPwm(rev1.HardwareIdCV5Output, rev1.DefaultVoltageOutputCalibration),
+ OutputVoltage6: common.NewNonPicoPwm(rev1.HardwareIdCV6Output, rev1.DefaultVoltageOutputCalibration),
+ DeviceRandomGenerator1: nil,
+ })
+}
diff --git a/internal/nonpico/rev1/site/index.html b/internal/nonpico/rev1/site/index.html
new file mode 100644
index 0000000..35d420f
--- /dev/null
+++ b/internal/nonpico/rev1/site/index.html
@@ -0,0 +1,268 @@
+
+
+
+
+
+ EuroPi Tester
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/nonpico/rev1/wsactivation.go b/internal/nonpico/rev1/wsactivation.go
new file mode 100644
index 0000000..57cff7f
--- /dev/null
+++ b/internal/nonpico/rev1/wsactivation.go
@@ -0,0 +1,175 @@
+//go:build !pico
+// +build !pico
+
+package rev1
+
+import (
+ "context"
+ "embed"
+ "encoding/json"
+ "io/fs"
+ "log"
+ "net/http"
+ _ "net/http/pprof"
+ "strconv"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/internal/nonpico/common"
+ "github.com/awonak/EuroPiGo/internal/nonpico/ws"
+)
+
+type WSActivation struct {
+ ctx context.Context
+ cancel context.CancelFunc
+ displayMode common.DisplayMode
+}
+
+func ActivateWebSocket(ctx context.Context) *WSActivation {
+ a := &WSActivation{}
+
+ a.Start(ctx)
+
+ return a
+}
+
+func (a *WSActivation) Shutdown() error {
+ if a.cancel != nil {
+ a.cancel()
+ }
+ return nil
+}
+
+//go:embed site
+var nonPicoSiteContent embed.FS
+
+func (a *WSActivation) Start(ctx context.Context) {
+ a.ctx, a.cancel = context.WithCancel(ctx)
+
+ setupDefaultState()
+
+ go func() {
+ defer a.cancel()
+
+ subFS, _ := fs.Sub(nonPicoSiteContent, "site")
+ http.Handle("/", http.FileServer(http.FS(subFS)))
+ http.HandleFunc("/ws", a.apiHandler)
+ if err := http.ListenAndServe(":8080", nil); err != nil {
+ panic(err)
+ }
+ }()
+}
+
+func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) {
+ log.Println(r.URL, "rev1.apiHandler")
+
+ if r.Body != nil {
+ // just in case someone sent us a body
+ defer r.Body.Close()
+ }
+
+ q := r.URL.Query()
+ dm, _ := strconv.Atoi(q.Get("displayMode"))
+ a.displayMode = common.DisplayMode(dm)
+
+ sock, err := ws.Upgrade(w, r)
+ if err != nil {
+ log.Println("failed to upgrade websocket connection:", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ defer sock.Close()
+
+ setupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) {
+ _ = sock.WriteJSON(voltageOutputMsg{
+ Kind: "voltageOutput",
+ HardwareId: id,
+ Voltage: voltage,
+ })
+ })
+
+ displayWidth, displayHeight := 128, 32
+ displayScreenOutputMsg := displayScreenOuptutMsg{
+ Kind: "displayScreenOutput",
+ Width: displayWidth,
+ Height: displayHeight,
+ Data: make([]byte, displayWidth*displayHeight*4),
+ }
+ setupDisplayOutputListener(func(id hal.HardwareId, op common.HwDisplayOp, params []int16) {
+ switch a.displayMode {
+ case common.DisplayModeCombined:
+ switch op {
+ case common.HwDisplayOpClearBuffer:
+ for i := range displayScreenOutputMsg.Data {
+ displayScreenOutputMsg.Data[i] = 0
+ }
+ case common.HwDisplayOpSetPixel:
+ y, x := int(params[1]), int(params[0])
+ if y < 0 || y >= displayHeight || x < 0 || x >= displayWidth {
+ break
+ }
+ pos := (int(params[1])*displayWidth + int(params[0])) * 4
+ displayScreenOutputMsg.Data[pos] = byte(params[2])
+ displayScreenOutputMsg.Data[pos+1] = byte(params[3])
+ displayScreenOutputMsg.Data[pos+2] = byte(params[4])
+ displayScreenOutputMsg.Data[pos+3] = byte(params[5])
+ case common.HwDisplayOpDisplay:
+ _ = sock.WriteJSON(displayScreenOutputMsg)
+ default:
+ }
+
+ default:
+ _ = sock.WriteJSON(displayOutputMsg{
+ Kind: "displayOutput",
+ HardwareId: id,
+ Op: op,
+ Params: params,
+ })
+ }
+ })
+
+ type kind struct {
+ Kind string `json:"kind"`
+ }
+
+ for {
+ // test for doneness
+ select {
+ case <-sock.Done():
+ break
+ default:
+ }
+
+ blob, err := sock.ReadMessage()
+ if err != nil {
+ break
+ }
+
+ var k kind
+ if err := json.Unmarshal(blob, &k); err != nil {
+ sock.SetError(err)
+ break
+ }
+
+ switch k.Kind {
+ case "setDigitalInput":
+ var di setDigitalInputMsg
+ if err := json.Unmarshal(blob, &di); err != nil {
+ sock.SetError(err)
+ break
+ }
+ setDigitalInput(di.HardwareId, di.Value)
+
+ case "setAnalogInput":
+ var ai setAnalogInputMsg
+ if err := json.Unmarshal(blob, &ai); err != nil {
+ sock.SetError(err)
+ break
+ }
+ setAnalogInput(ai.HardwareId, ai.Voltage)
+
+ default:
+ // ignore
+ }
+ }
+}
diff --git a/internal/nonpico/revisiondetection.go b/internal/nonpico/revisiondetection.go
new file mode 100644
index 0000000..19833fd
--- /dev/null
+++ b/internal/nonpico/revisiondetection.go
@@ -0,0 +1,12 @@
+//go:build !pico
+// +build !pico
+
+package nonpico
+
+import "github.com/awonak/EuroPiGo/hardware/hal"
+
+var detectedRevision hal.Revision
+
+func DetectRevision() hal.Revision {
+ return detectedRevision
+}
diff --git a/internal/nonpico/revisiondetection_rev0.go b/internal/nonpico/revisiondetection_rev0.go
new file mode 100644
index 0000000..aac032e
--- /dev/null
+++ b/internal/nonpico/revisiondetection_rev0.go
@@ -0,0 +1,13 @@
+//go:build !pico && (revision0 || europiproto || europiprototype)
+// +build !pico
+// +build revision0 europiproto europiprototype
+
+package nonpico
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+func init() {
+ detectedRevision = hal.Revision0
+}
diff --git a/internal/nonpico/revisiondetection_rev1.go b/internal/nonpico/revisiondetection_rev1.go
new file mode 100644
index 0000000..9aaeeb2
--- /dev/null
+++ b/internal/nonpico/revisiondetection_rev1.go
@@ -0,0 +1,13 @@
+//go:build !pico && (revision1 || europi)
+// +build !pico
+// +build revision1 europi
+
+package nonpico
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+func init() {
+ detectedRevision = hal.Revision1
+}
diff --git a/internal/nonpico/revisiondetection_rev2.go b/internal/nonpico/revisiondetection_rev2.go
new file mode 100644
index 0000000..64d4f2e
--- /dev/null
+++ b/internal/nonpico/revisiondetection_rev2.go
@@ -0,0 +1,13 @@
+//go:build !pico && (revision2 || europix)
+// +build !pico
+// +build revision2 europix
+
+package nonpico
+
+import (
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+func init() {
+ detectedRevision = hal.Revision2
+}
diff --git a/internal/nonpico/ws/websocket.go b/internal/nonpico/ws/websocket.go
new file mode 100644
index 0000000..69105df
--- /dev/null
+++ b/internal/nonpico/ws/websocket.go
@@ -0,0 +1,101 @@
+//go:build !pico
+// +build !pico
+
+package ws
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+const (
+ pongWait = time.Second * 60
+)
+
+type WebSocket struct {
+ conn *websocket.Conn
+ mu sync.Mutex
+ ctx context.Context
+ cancel context.CancelFunc
+ err error
+}
+
+func (w *WebSocket) Done() <-chan struct{} {
+ if w.ctx == nil {
+ return nil
+ }
+
+ return w.ctx.Done()
+}
+
+func (w *WebSocket) WriteJSON(msg any) error {
+ if w.conn == nil {
+ return errors.New("connection not established")
+ }
+
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ err := w.conn.WriteJSON(msg)
+ if err != nil {
+ w.SetError(err)
+ }
+ return err
+}
+
+func (w *WebSocket) ReadMessage() ([]byte, error) {
+ _, blob, err := w.conn.ReadMessage()
+ if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
+ w.SetError(err)
+ }
+ return blob, err
+}
+
+func (w *WebSocket) SetError(err error) {
+ w.err = err
+ if w.cancel != nil {
+ w.cancel()
+ }
+}
+
+func (w *WebSocket) Close() error {
+ if w.cancel != nil {
+ w.cancel()
+ }
+
+ if w.conn == nil {
+ return nil
+ }
+
+ return w.conn.Close()
+}
+
+var upgrader = websocket.Upgrader{}
+
+func Upgrade(w http.ResponseWriter, r *http.Request) (*WebSocket, error) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ ctx, cancel := context.WithCancel(r.Context())
+
+ conn.SetReadLimit(2048)
+ _ = conn.SetWriteDeadline(time.Now().Add(pongWait))
+ conn.SetPongHandler(func(appData string) error {
+ return conn.SetWriteDeadline(time.Now().Add(pongWait))
+ })
+
+ ws := &WebSocket{
+ conn: conn,
+ ctx: ctx,
+ cancel: cancel,
+ }
+
+ return ws, nil
+}
diff --git a/internal/nonpico/wsactivator.go b/internal/nonpico/wsactivator.go
new file mode 100644
index 0000000..a4309df
--- /dev/null
+++ b/internal/nonpico/wsactivator.go
@@ -0,0 +1,28 @@
+//go:build !pico
+// +build !pico
+
+package nonpico
+
+import (
+ "context"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/internal/nonpico/rev0"
+ "github.com/awonak/EuroPiGo/internal/nonpico/rev1"
+)
+
+type WSActivation interface {
+ Shutdown() error
+}
+
+func ActivateWebSocket(ctx context.Context, revision hal.Revision) WSActivation {
+ switch revision {
+ case hal.Revision0:
+ return rev0.ActivateWebSocket(ctx)
+ case hal.Revision1:
+ return rev1.ActivateWebSocket(ctx)
+ // TODO: add rev2
+ default:
+ return nil
+ }
+}
diff --git a/internal/pico/adc.go b/internal/pico/adc.go
new file mode 100644
index 0000000..2ad5a17
--- /dev/null
+++ b/internal/pico/adc.go
@@ -0,0 +1,42 @@
+//go:build pico
+// +build pico
+
+package pico
+
+import (
+ "machine"
+ "runtime/interrupt"
+ "sync"
+)
+
+var (
+ adcOnce sync.Once
+)
+
+type picoAdc struct {
+ adc machine.ADC
+}
+
+func newPicoAdc(pin machine.Pin) *picoAdc {
+ adcOnce.Do(machine.InitADC)
+
+ adc := &picoAdc{
+ adc: machine.ADC{Pin: pin},
+ }
+ adc.adc.Configure(machine.ADCConfig{})
+ return adc
+}
+
+func (a *picoAdc) Get(samples int) uint16 {
+ if samples == 0 {
+ return 0
+ }
+
+ var sum int
+ state := interrupt.Disable()
+ for i := 0; i < samples; i++ {
+ sum += int(a.adc.Get())
+ }
+ interrupt.Restore(state)
+ return uint16(sum / samples)
+}
diff --git a/internal/pico/digitalreader.go b/internal/pico/digitalreader.go
new file mode 100644
index 0000000..db4442e
--- /dev/null
+++ b/internal/pico/digitalreader.go
@@ -0,0 +1,52 @@
+//go:build pico
+// +build pico
+
+package pico
+
+import (
+ "machine"
+ "runtime/interrupt"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+type picoDigitalReader struct {
+ pin machine.Pin
+}
+
+func newPicoDigitalReader(pin machine.Pin) *picoDigitalReader {
+ dr := &picoDigitalReader{
+ pin: pin,
+ }
+ dr.pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
+ return dr
+}
+
+func (d *picoDigitalReader) Get() bool {
+ state := interrupt.Disable()
+ // Invert signal to match expected behavior.
+ v := !d.pin.Get()
+ interrupt.Restore(state)
+ return v
+}
+
+func (d *picoDigitalReader) SetHandler(changes hal.ChangeFlags, handler func()) {
+ pinChange := d.convertChangeFlags(changes)
+
+ state := interrupt.Disable()
+ d.pin.SetInterrupt(pinChange, func(machine.Pin) {
+ handler()
+ })
+ interrupt.Restore(state)
+}
+
+func (d *picoDigitalReader) convertChangeFlags(changes hal.ChangeFlags) machine.PinChange {
+ var pinChange machine.PinChange
+ if (changes & hal.ChangeRising) != 0 {
+ pinChange |= machine.PinFalling
+ }
+ if (changes & hal.ChangeFalling) != 0 {
+ pinChange |= machine.PinRising
+ }
+ return pinChange
+}
diff --git a/internal/pico/display.go b/internal/pico/display.go
new file mode 100644
index 0000000..617e701
--- /dev/null
+++ b/internal/pico/display.go
@@ -0,0 +1,58 @@
+//go:build pico
+// +build pico
+
+package pico
+
+import (
+ "image/color"
+ "machine"
+
+ "tinygo.org/x/drivers/ssd1306"
+)
+
+const (
+ oledFreq = machine.KHz * 400
+ oledAddr = ssd1306.Address_128_32
+ oledWidth = 128
+ oledHeight = 32
+)
+
+type picoDisplayOutput struct {
+ dev ssd1306.Device
+}
+
+func newPicoDisplayOutput(channel *machine.I2C, sdaPin, sclPin machine.Pin) *picoDisplayOutput {
+ channel.Configure(machine.I2CConfig{
+ Frequency: oledFreq,
+ SDA: sdaPin,
+ SCL: sclPin,
+ })
+
+ display := ssd1306.NewI2C(channel)
+ display.Configure(ssd1306.Config{
+ Address: oledAddr,
+ Width: oledWidth,
+ Height: oledHeight,
+ })
+
+ dp := &picoDisplayOutput{
+ dev: display,
+ }
+
+ return dp
+}
+
+func (d *picoDisplayOutput) ClearBuffer() {
+ d.dev.ClearBuffer()
+}
+
+func (d *picoDisplayOutput) Size() (x, y int16) {
+ return d.dev.Size()
+}
+func (d *picoDisplayOutput) SetPixel(x, y int16, c color.RGBA) {
+ d.dev.SetPixel(x, y, c)
+}
+
+func (d *picoDisplayOutput) Display() error {
+ return d.dev.Display()
+}
diff --git a/internal/pico/pico.go b/internal/pico/pico.go
new file mode 100644
index 0000000..4d10075
--- /dev/null
+++ b/internal/pico/pico.go
@@ -0,0 +1,7 @@
+package pico
+
+// This file is required for compilation to occur. Do not remove it
+// or the empty init function at the bottom of the file.
+
+func init() {
+}
diff --git a/internal/pico/platform.go b/internal/pico/platform.go
new file mode 100644
index 0000000..f7cb297
--- /dev/null
+++ b/internal/pico/platform.go
@@ -0,0 +1,72 @@
+//go:build pico
+// +build pico
+
+package pico
+
+import (
+ "machine"
+
+ "github.com/awonak/EuroPiGo/hardware"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/hardware/rev0"
+ "github.com/awonak/EuroPiGo/hardware/rev1"
+)
+
+// EuroPi Prototype
+func initRevision0() {
+ rev0.Initialize(rev0.InitializationParameters{
+ InputButton1: newPicoDigitalReader(machine.GPIO15),
+ InputButton2: newPicoDigitalReader(machine.GPIO18),
+ InputKnob1: newPicoAdc(machine.ADC2), // machine.GPIO28
+ InputKnob2: newPicoAdc(machine.ADC1), // machine.GPIO27
+ OutputAnalog1: newPicoPwm(machine.PWM2, machine.GPIO21),
+ OutputAnalog2: newPicoPwm(machine.PWM3, machine.GPIO22),
+ OutputAnalog3: newPicoPwm(machine.PWM1, machine.GPIO19),
+ OutputAnalog4: newPicoPwm(machine.PWM2, machine.GPIO20),
+ OutputDigital1: newPicoPwm(machine.PWM7, machine.GPIO14),
+ OutputDigital2: newPicoPwm(machine.PWM5, machine.GPIO11),
+ OutputDigital3: newPicoPwm(machine.PWM5, machine.GPIO10),
+ OutputDigital4: newPicoPwm(machine.PWM3, machine.GPIO7),
+ DeviceRandomGenerator1: &picoRnd{},
+ })
+}
+
+// EuroPi (original)
+func initRevision1() {
+ rev1.Initialize(rev1.InitializationParameters{
+ InputDigital1: newPicoDigitalReader(machine.GPIO22),
+ InputAnalog1: newPicoAdc(machine.ADC0), // machine.GPIO26
+ OutputDisplay1: newPicoDisplayOutput(machine.I2C0, machine.GPIO0, machine.GPIO1),
+ InputButton1: newPicoDigitalReader(machine.GPIO4),
+ InputButton2: newPicoDigitalReader(machine.GPIO5),
+ InputKnob1: newPicoAdc(machine.ADC1), // machine.GPIO27
+ InputKnob2: newPicoAdc(machine.ADC2), // machine.GPIO28
+ OutputVoltage1: newPicoPwm(machine.PWM2, machine.GPIO21),
+ OutputVoltage2: newPicoPwm(machine.PWM2, machine.GPIO20),
+ OutputVoltage3: newPicoPwm(machine.PWM0, machine.GPIO16),
+ OutputVoltage4: newPicoPwm(machine.PWM0, machine.GPIO17),
+ OutputVoltage5: newPicoPwm(machine.PWM1, machine.GPIO18),
+ OutputVoltage6: newPicoPwm(machine.PWM1, machine.GPIO19),
+ DeviceRandomGenerator1: &picoRnd{},
+ })
+}
+
+// EuroPi-X
+func initRevision2() {
+ // TODO: initialize hardware
+}
+
+func init() {
+ hardware.OnRevisionDetected(func(revision hal.Revision) {
+ switch revision {
+ case hal.Revision0:
+ initRevision0()
+ case hal.Revision1:
+ initRevision1()
+ case hal.Revision2:
+ initRevision2()
+ default:
+ }
+ hardware.SetReady()
+ })
+}
diff --git a/internal/pico/pwm.go b/internal/pico/pwm.go
new file mode 100644
index 0000000..ce5259f
--- /dev/null
+++ b/internal/pico/pwm.go
@@ -0,0 +1,110 @@
+//go:build pico
+// +build pico
+
+package pico
+
+import (
+ "fmt"
+ "machine"
+ "math"
+ "runtime/interrupt"
+ "sync/atomic"
+ "time"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/hardware/rev0"
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+type picoPwm struct {
+ pwm pwmGroup
+ pin machine.Pin
+ ch uint8
+ v uint32
+ period time.Duration
+ monopolar bool
+ cal lerp.Remapper32[float32, uint16]
+}
+
+// pwmGroup is an interface for interacting with a machine.pwmGroup
+type pwmGroup interface {
+ Configure(config machine.PWMConfig) error
+ Channel(pin machine.Pin) (channel uint8, err error)
+ Top() uint32
+ SetTop(top uint32)
+ Get(channel uint8) uint32
+ Set(channel uint8, value uint32)
+ SetPeriod(period uint64) error
+}
+
+type picoPwmMode int
+
+func newPicoPwm(pwm pwmGroup, pin machine.Pin) *picoPwm {
+ p := &picoPwm{
+ pwm: pwm,
+ pin: pin,
+ period: rev0.DefaultPWMPeriod,
+ // NOTE: cal must be set non-nil by Configure() at least 1 time
+ }
+ return p
+}
+
+func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error {
+ state := interrupt.Disable()
+ defer interrupt.Restore(state)
+
+ if config.Period != 0 {
+ p.period = config.Period
+ }
+
+ err := p.pwm.Configure(machine.PWMConfig{
+ Period: uint64(p.period.Nanoseconds()),
+ })
+ if err != nil {
+ return fmt.Errorf("pwm Configure error: %w", err)
+ }
+
+ if config.Calibration != nil {
+ p.cal = config.Calibration
+ }
+
+ if any(p.cal) == nil {
+ return fmt.Errorf("pwm Configure error: Calibration must be non-nil")
+ }
+
+ p.pwm.SetTop(uint32(p.cal.OutputMaximum()))
+ ch, err := p.pwm.Channel(p.pin)
+ if err != nil {
+ return fmt.Errorf("pwm Channel error: %w", err)
+ }
+ p.ch = ch
+
+ p.monopolar = config.Monopolar
+
+ return nil
+}
+
+func (p *picoPwm) Set(v float32) {
+ if p.monopolar {
+ if v < 0.0 {
+ v = -v
+ }
+ }
+ volts := p.cal.Remap(v)
+ state := interrupt.Disable()
+ p.pwm.Set(p.ch, uint32(volts))
+ interrupt.Restore(state)
+ atomic.StoreUint32(&p.v, math.Float32bits(v))
+}
+
+func (p *picoPwm) Get() float32 {
+ return math.Float32frombits(atomic.LoadUint32(&p.v))
+}
+
+func (p *picoPwm) MinVoltage() float32 {
+ return p.cal.InputMinimum()
+}
+
+func (p *picoPwm) MaxVoltage() float32 {
+ return p.cal.InputMaximum()
+}
diff --git a/internal/pico/revisiondetection.go b/internal/pico/revisiondetection.go
new file mode 100644
index 0000000..f5741e4
--- /dev/null
+++ b/internal/pico/revisiondetection.go
@@ -0,0 +1,55 @@
+//go:build pico
+// +build pico
+
+package pico
+
+import (
+ "machine"
+ "runtime/interrupt"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+func configureAndGetPinLevel(pin machine.Pin) bool {
+ pin.Configure(machine.PinConfig{
+ Mode: machine.PinInputPulldown,
+ })
+ state := interrupt.Disable()
+ level := pin.Get()
+ interrupt.Restore(state)
+ return level
+}
+
+func GetDetectedRevisionBits() int {
+ var revision int
+ if gp6 := configureAndGetPinLevel(machine.GPIO6); gp6 {
+ revision |= 0b0001
+ }
+ if gp7 := configureAndGetPinLevel(machine.GPIO7); gp7 {
+ revision |= 0b0010
+ }
+ if gp8 := configureAndGetPinLevel(machine.GPIO8); gp8 {
+ revision |= 0b0100
+ }
+ if gp9 := configureAndGetPinLevel(machine.GPIO9); gp9 {
+ revision |= 0b1000
+ }
+ return revision
+}
+
+func DetectRevision() hal.Revision {
+ revBits := GetDetectedRevisionBits()
+ switch revBits {
+ case 0: // 0000
+ if rev1AsRev0 {
+ return hal.Revision0
+ }
+ return hal.Revision1
+ case 1: // 0001
+ return hal.Revision2
+ default: // not yet known or maybe Revision0 / EuroPi-Proto?
+ return hal.RevisionUnknown
+ }
+}
+
+var rev1AsRev0 bool
diff --git a/internal/pico/revisiondetection_rev0.go b/internal/pico/revisiondetection_rev0.go
new file mode 100644
index 0000000..9a91603
--- /dev/null
+++ b/internal/pico/revisiondetection_rev0.go
@@ -0,0 +1,9 @@
+//go:build pico && (revision0 || europiproto || europiprototype)
+// +build pico
+// +build revision0 europiproto europiprototype
+
+package pico
+
+func init() {
+ rev1AsRev0 = true
+}
diff --git a/internal/pico/rnd.go b/internal/pico/rnd.go
new file mode 100644
index 0000000..0289324
--- /dev/null
+++ b/internal/pico/rnd.go
@@ -0,0 +1,21 @@
+//go:build pico
+// +build pico
+
+package pico
+
+import (
+ "machine"
+ "math/rand"
+
+ "github.com/awonak/EuroPiGo/hardware/hal"
+)
+
+type picoRnd struct{}
+
+func (r *picoRnd) Configure(config hal.RandomGeneratorConfig) error {
+ xl, _ := machine.GetRNG()
+ xh, _ := machine.GetRNG()
+ x := int64(xh)<<32 | int64(xl)
+ rand.Seed(x)
+ return nil
+}
diff --git a/scripts/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go
similarity index 65%
rename from scripts/clockwerk/clockwerk.go
rename to internal/projects/clockwerk/clockwerk.go
index 217158c..3d151d1 100644
--- a/scripts/clockwerk/clockwerk.go
+++ b/internal/projects/clockwerk/clockwerk.go
@@ -28,13 +28,18 @@
package main
import (
- "machine"
"strconv"
"time"
"tinygo.org/x/tinydraw"
+ "tinygo.org/x/tinyfont/proggy"
europi "github.com/awonak/EuroPiGo"
+ "github.com/awonak/EuroPiGo/clamp"
+ "github.com/awonak/EuroPiGo/experimental/draw"
+ "github.com/awonak/EuroPiGo/experimental/fontwriter"
+ "github.com/awonak/EuroPiGo/hardware/hal"
+ "github.com/awonak/EuroPiGo/lerp"
)
const (
@@ -49,6 +54,7 @@ var (
// Positive values are multiplications and negative values are divisions.
DefaultFactor = [6]int{1, 2, 4, -2, -4, -8}
FactorChoices []int
+ DefaultFont = &proggy.TinySZ8pt7b
)
func init() {
@@ -74,8 +80,11 @@ type Clockwerk struct {
period time.Duration
clocks [6]int
resets [6]chan uint8
+ writer fontwriter.Writer
*europi.EuroPi
+ bpmLerp lerp.Lerper32[uint16]
+ factorLerp lerp.Lerper32[int]
}
func (c *Clockwerk) editParams() {
@@ -95,7 +104,7 @@ func (c *Clockwerk) editParams() {
func (c *Clockwerk) readBPM() uint16 {
// Provide a range of 59 - 240 bpm. bpm < 60 will switch to external clock.
- _bpm := c.K1.Range((MaxBPM+1)-(MinBPM-2)) + MinBPM - 1
+ _bpm := c.bpmLerp.ClampedLerpRound(c.K1.Percent())
if _bpm < MinBPM {
c.external = true
_bpm = 0
@@ -110,7 +119,8 @@ func (c *Clockwerk) readBPM() uint16 {
}
func (c *Clockwerk) readFactor() int {
- return FactorChoices[c.K2.Range(uint16(len(FactorChoices)))]
+ idx := c.factorLerp.ClampedLerpRound(c.K2.Percent())
+ return FactorChoices[idx]
}
func (c *Clockwerk) startClocks() {
@@ -152,13 +162,13 @@ func (c *Clockwerk) clock(i uint8, reset chan uint8) {
high, low := c.clockPulseWidth(c.clocks[i])
- c.CV[i].On()
+ c.CV()[i].SetCV(1.0)
t = t.Add(high)
- time.Sleep(t.Sub(time.Now()))
+ time.Sleep(time.Since(t))
- c.CV[i].Off()
+ c.CV()[i].SetCV(0.0)
t = t.Add(low)
- time.Sleep(t.Sub(time.Now()))
+ time.Sleep(time.Since(t))
}
}
@@ -185,16 +195,18 @@ func (c *Clockwerk) updateDisplay() {
return
}
c.displayShouldUpdate = false
- c.Display.ClearBuffer()
+ c.OLED.ClearBuffer()
// Master clock and pulse width.
var external string
if c.external {
external = "^"
}
- c.Display.WriteLine(external+"BPM: "+strconv.Itoa(int(c.bpm)), 2, 8)
+ c.writer.WriteLine(external+"BPM: "+strconv.Itoa(int(c.bpm)), 2, 8, draw.White)
// Display each clock multiplication or division setting.
+ dispWidth, _ := c.OLED.Size()
+ divWidth := int(dispWidth) / len(c.clocks)
for i, factor := range c.clocks {
text := " 1"
switch {
@@ -203,66 +215,98 @@ func (c *Clockwerk) updateDisplay() {
case factor > 1:
text = "x" + strconv.Itoa(factor)
}
- c.Display.WriteLine(text, int16(i*europi.OLEDWidth/len(c.clocks))+2, 26)
+ c.writer.WriteLine(text, int16(i*divWidth)+2, 26, draw.White)
}
- xWidth := int16(europi.OLEDWidth / len(c.clocks))
+ xWidth := int16(divWidth)
xOffset := int16(c.selected) * xWidth
// TODO: replace box with chevron.
- tinydraw.Rectangle(c.Display, xOffset, 16, xWidth, 16, europi.White)
+ _ = tinydraw.Rectangle(c.OLED, xOffset, 16, xWidth, 16, draw.White)
- c.Display.Display()
+ _ = c.OLED.Display()
}
-func main() {
- c := Clockwerk{
- EuroPi: europi.New(),
- clocks: DefaultFactor,
- displayShouldUpdate: true,
+var app Clockwerk
+
+func appStart(e *europi.EuroPi) {
+ app.EuroPi = e
+ app.clocks = DefaultFactor
+ app.displayShouldUpdate = true
+ app.writer = fontwriter.Writer{
+ Display: e.OLED,
+ Font: DefaultFont,
}
+ app.bpmLerp = lerp.NewLerp32[uint16](MinBPM-1, MaxBPM)
+ app.factorLerp = lerp.NewLerp32(0, len(FactorChoices)-1)
// Lower range value can have lower sample size
- c.K1.Samples(500)
- c.K2.Samples(20)
+ _ = app.K1.Configure(hal.AnalogInputConfig{
+ Samples: 500,
+ })
+ _ = app.K2.Configure(hal.AnalogInputConfig{
+ Samples: 20,
+ })
- c.DI.Handler(func(pin machine.Pin) {
+ app.DI.Handler(func(_ bool, deltaTime time.Duration) {
// Measure current period between clock pulses.
- c.period = time.Now().Sub(c.DI.LastInput())
+ app.period = deltaTime
})
// Move clock config option to the left.
- c.B1.Handler(func(p machine.Pin) {
- if c.B2.Value() {
- c.doClockReset = true
+ app.B1.Handler(func(_ bool, deltaTime time.Duration) {
+ if app.B2.Value() {
+ app.doClockReset = true
return
}
- c.selected = uint8(europi.Clamp(int(c.selected)-1, 0, len(c.clocks)))
- c.displayShouldUpdate = true
+ app.selected = uint8(clamp.Clamp(int(app.selected)-1, 0, len(app.clocks)))
+ app.displayShouldUpdate = true
})
// Move clock config option to the right.
- c.B2.Handler(func(p machine.Pin) {
- if c.B1.Value() {
- c.doClockReset = true
+ app.B2.Handler(func(_ bool, deltaTime time.Duration) {
+ if app.B1.Value() {
+ app.doClockReset = true
return
}
- c.selected = uint8(europi.Clamp(int(c.selected)+1, 0, len(c.clocks)-1))
- c.displayShouldUpdate = true
+ app.selected = uint8(clamp.Clamp(int(app.selected)+1, 0, len(app.clocks)-1))
+ app.displayShouldUpdate = true
})
// Init parameter configs based on current knob positions.
- c.bpm = c.readBPM()
- c.prevk2 = c.readFactor()
+ app.bpm = app.readBPM()
+ app.prevk2 = app.readFactor()
- c.startClocks()
+ app.startClocks()
+}
+
+func mainLoop() {
+ if app.doClockReset {
+ app.doClockReset = false
+ app.resetClocks()
+ app.displayShouldUpdate = true
+ }
+ europi.DebugMemoryUsage()
+}
+
+func main() {
+ e, _ := europi.New().(*europi.EuroPi)
+ if e == nil {
+ panic("europi not detected")
+ }
+ // since we're not using a full bootstrap, manually activate the webservice (this is a no-op on pico)
+ if ws := europi.ActivateNonPicoWS(e.Context(), e); ws != nil {
+ defer func() {
+ _ = ws.Shutdown()
+ }()
+ }
+
+ appStart(e)
+
+ // Check for clock updates every 2 seconds.
+ ticker := time.NewTicker(ResetDelay)
+ defer ticker.Stop()
for {
- // Check for clock updates every 2 seconds.
- time.Sleep(ResetDelay)
- if c.doClockReset {
- c.doClockReset = false
- c.resetClocks()
- c.displayShouldUpdate = true
- }
- europi.DebugMemoryUsage()
+ <-ticker.C
+ mainLoop()
}
}
diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go
new file mode 100644
index 0000000..0bdf517
--- /dev/null
+++ b/internal/projects/diagnostics/diagnostics.go
@@ -0,0 +1,108 @@
+// Diagnostics is a script for demonstrating all main interactions with the europi-go firmware.
+package main
+
+import (
+ "fmt"
+ "time"
+
+ "tinygo.org/x/tinydraw"
+ "tinygo.org/x/tinyfont/proggy"
+
+ europi "github.com/awonak/EuroPiGo"
+ "github.com/awonak/EuroPiGo/experimental/draw"
+ "github.com/awonak/EuroPiGo/experimental/fontwriter"
+)
+
+type MyApp struct {
+ knobsDisplayPercent bool
+ prevK1 uint16
+ prevK2 uint16
+ staticCv int
+ prevStaticCv int
+}
+
+var myApp MyApp
+
+func appStart(e *europi.EuroPi) {
+ myApp.staticCv = 5
+
+ // Demonstrate adding a IRQ handler to B1 and B2.
+ e.B1.Handler(func(_ bool, _ time.Duration) {
+ myApp.knobsDisplayPercent = !myApp.knobsDisplayPercent
+ })
+
+ e.B2.Handler(func(_ bool, _ time.Duration) {
+ myApp.staticCv = (myApp.staticCv + 1) % int(e.K1.MaxVoltage())
+ })
+}
+
+var (
+ DefaultFont = &proggy.TinySZ8pt7b
+)
+
+func mainLoop(e *europi.EuroPi) {
+ e.OLED.ClearBuffer()
+
+ // Highlight the border of the oled display.
+ _ = tinydraw.Rectangle(e.OLED, 0, 0, 128, 32, draw.White)
+
+ writer := fontwriter.Writer{
+ Display: e.OLED,
+ Font: DefaultFont,
+ }
+
+ // Display analog and digital input values.
+ inputText := fmt.Sprintf("din: %5v ain: %2.2f ", e.DI.Value(), e.AI.Percent())
+ writer.WriteLine(inputText, 3, 8, draw.White)
+
+ // Display knob values based on app state.
+ var knobText string
+ if myApp.knobsDisplayPercent {
+ knobText = fmt.Sprintf("K1: %0.2f K2: %0.2f", e.K1.Percent(), e.K2.Percent())
+ } else {
+ knobText = fmt.Sprintf("K1: %3d K2: %3d", int(e.K1.Percent()*100), int(e.K2.Percent()*100))
+ }
+ writer.WriteLine(knobText, 3, 18, draw.White)
+
+ // Show current button press state.
+ writer.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", e.B1.Value(), e.B2.Value()), 3, 28, draw.White)
+
+ _ = e.OLED.Display()
+
+ // Set voltage values for the 6 CV outputs.
+ if kv := uint16(e.K1.Percent() * float32(1<<12)); kv != myApp.prevK1 {
+ e.CV1.SetVoltage(e.K1.ReadVoltage())
+ e.CV4.SetVoltage(e.CV4.MaxVoltage() - e.K1.ReadVoltage())
+ myApp.prevK1 = kv
+ }
+ if kv := uint16(e.K2.Percent() * float32(1<<12)); kv != myApp.prevK2 {
+ e.CV2.SetVoltage(e.K2.ReadVoltage())
+ e.CV5.SetVoltage(e.CV5.MaxVoltage() - e.K2.ReadVoltage())
+ myApp.prevK2 = kv
+ }
+ e.CV3.SetCV(1.0)
+ if myApp.staticCv != myApp.prevStaticCv {
+ e.CV6.SetVoltage(float32(myApp.staticCv))
+ myApp.prevStaticCv = myApp.staticCv
+ }
+}
+
+func main() {
+ e, _ := europi.New().(*europi.EuroPi)
+ if e == nil {
+ panic("europi not detected")
+ }
+
+ // since we're not using a full bootstrap, manually activate the webservice (this is a no-op on pico)
+ if ws := europi.ActivateNonPicoWS(e.Context(), e); ws != nil {
+ defer func() {
+ _ = ws.Shutdown()
+ }()
+ }
+
+ appStart(e)
+ for {
+ mainLoop(e)
+ time.Sleep(time.Millisecond)
+ }
+}
diff --git a/lerp/lerp.go b/lerp/lerp.go
new file mode 100644
index 0000000..bd6e9d2
--- /dev/null
+++ b/lerp/lerp.go
@@ -0,0 +1,24 @@
+package lerp
+
+type Lerpable interface {
+ ~int8 | ~int16 | ~int32 | ~int64 | ~int | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64
+}
+
+type Float interface {
+ ~float32 | ~float64
+}
+
+type Lerper[T Lerpable, F Float] interface {
+ Lerp(t F) T
+ ClampedLerp(t F) T
+ LerpRound(t F) T
+ ClampedLerpRound(t F) T
+ InverseLerp(v T) F
+ ClampedInverseLerp(v T) F
+ OutputMinimum() T
+ OutputMaximum() T
+}
+
+type Lerper32[T Lerpable] Lerper[T, float32]
+
+type Lerper64[T Lerpable] Lerper[T, float64]
diff --git a/lerp/lerp32.go b/lerp/lerp32.go
new file mode 100644
index 0000000..ba1a9f1
--- /dev/null
+++ b/lerp/lerp32.go
@@ -0,0 +1,53 @@
+package lerp
+
+import "github.com/awonak/EuroPiGo/clamp"
+
+type lerp32[T Lerpable] struct {
+ b T
+ r float32
+}
+
+func NewLerp32[T Lerpable](min, max T) Lerper32[T] {
+ return lerp32[T]{
+ b: min,
+ r: float32(max - min),
+ }
+}
+
+func (l lerp32[T]) Lerp(t float32) T {
+ return T(t*l.r) + l.b
+}
+
+func (l lerp32[T]) ClampedLerp(t float32) T {
+ return clamp.Clamp(T(t*l.r)+l.b, l.b, T(l.r)+l.b)
+}
+
+func (l lerp32[T]) LerpRound(t float32) T {
+ return T(t*l.r+0.5) + l.b
+}
+
+func (l lerp32[T]) ClampedLerpRound(t float32) T {
+ return clamp.Clamp(T(t*l.r+0.5)+l.b, l.b, T(l.r)+l.b)
+}
+
+func (l lerp32[T]) InverseLerp(v T) float32 {
+ if l.r != 0.0 {
+ return (float32(v) - float32(l.b)) / l.r
+ }
+ return 0.0
+}
+
+func (l lerp32[T]) ClampedInverseLerp(v T) float32 {
+ if l.r != 0.0 {
+ return clamp.Clamp((float32(v)-float32(l.b))/l.r, 0.0, 1.0)
+ }
+ return 0.0
+}
+
+func (l lerp32[T]) OutputMinimum() T {
+ return l.b
+}
+
+func (l lerp32[T]) OutputMaximum() T {
+ return T(l.r) + l.b
+}
diff --git a/lerp/lerp32_test.go b/lerp/lerp32_test.go
new file mode 100644
index 0000000..e4e76e3
--- /dev/null
+++ b/lerp/lerp32_test.go
@@ -0,0 +1,280 @@
+package lerp_test
+
+import (
+ "testing"
+
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+func TestLerp32(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
+ min, max := 0, 10
+ if actual := lerp.NewLerp32(min, max); actual == nil {
+ t.Fatalf("Lerp32[%v, %v] NewLerp32: expected[non-nil] actual[nil]", min, max)
+ }
+ })
+
+ t.Run("Lerp", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := min, l.Lerp(0.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] Lerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := max, l.Lerp(1.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] Lerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // Lerp() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := -2*max, l.Lerp(-2.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] Lerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := 2*max, l.Lerp(2.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] Lerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("ClampedLerp", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := min, l.ClampedLerp(0.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := max, l.ClampedLerp(1.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := min, l.ClampedLerp(-2.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := max, l.ClampedLerp(2.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("LerpRound", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := min, l.LerpRound(0.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] LerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := max, l.LerpRound(1.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] LerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // LerpRound() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := -2*max+1, l.LerpRound(-2.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] LerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := 2*max, l.LerpRound(2.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] LerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("ClampedLerpRound", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := min, l.ClampedLerpRound(0.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedLerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := max, l.ClampedLerpRound(1.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedLerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // ClampedLerpRound() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := min, l.ClampedLerpRound(-2.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedLerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := max, l.ClampedLerpRound(2.0); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedLerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("InverseLerp", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(0.0), l.InverseLerp(min); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Zero", func(t *testing.T) {
+ min, max := 5, 5
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(0.0), l.InverseLerp(max); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(1.0), l.InverseLerp(max); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // InverseLerp() will work as an inverse linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(-0.2), l.InverseLerp(-2); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(1.2), l.InverseLerp(12); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("ClampedInverseLerp", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(0.0), l.ClampedInverseLerp(min); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Zero", func(t *testing.T) {
+ min, max := 5, 5
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(0.0), l.ClampedInverseLerp(max); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(1.0), l.ClampedInverseLerp(max); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(0.0), l.ClampedInverseLerp(-2); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := float32(1.0), l.ClampedInverseLerp(12); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("OutputMinimum", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := min, l.OutputMinimum(); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] OutputMinimum: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("OutputMaximum", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp32(min, max)
+ if expected, actual := max, l.OutputMaximum(); actual != expected {
+ t.Fatalf("Lerp32[%v, %v] OutputMaximum: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+}
diff --git a/lerp/lerp64.go b/lerp/lerp64.go
new file mode 100644
index 0000000..9c0ba9b
--- /dev/null
+++ b/lerp/lerp64.go
@@ -0,0 +1,53 @@
+package lerp
+
+import "github.com/awonak/EuroPiGo/clamp"
+
+type lerp64[T Lerpable] struct {
+ b T
+ r float64
+}
+
+func NewLerp64[T Lerpable](min, max T) Lerper64[T] {
+ return lerp64[T]{
+ b: min,
+ r: float64(max - min),
+ }
+}
+
+func (l lerp64[T]) Lerp(t float64) T {
+ return T(t*l.r) + l.b
+}
+
+func (l lerp64[T]) ClampedLerp(t float64) T {
+ return clamp.Clamp(T(t*l.r)+l.b, l.b, T(l.r)+l.b)
+}
+
+func (l lerp64[T]) LerpRound(t float64) T {
+ return T(t*l.r+0.5) + l.b
+}
+
+func (l lerp64[T]) ClampedLerpRound(t float64) T {
+ return clamp.Clamp(T(t*l.r+0.5)+l.b, l.b, T(l.r)+l.b)
+}
+
+func (l lerp64[T]) InverseLerp(v T) float64 {
+ if l.r != 0.0 {
+ return (float64(v) - float64(l.b)) / l.r
+ }
+ return 0.0
+}
+
+func (l lerp64[T]) ClampedInverseLerp(v T) float64 {
+ if l.r != 0.0 {
+ return clamp.Clamp((float64(v)-float64(l.b))/l.r, 0.0, 1.0)
+ }
+ return 0.0
+}
+
+func (l lerp64[T]) OutputMinimum() T {
+ return l.b
+}
+
+func (l lerp64[T]) OutputMaximum() T {
+ return T(l.r) + l.b
+}
diff --git a/lerp/lerp64_test.go b/lerp/lerp64_test.go
new file mode 100644
index 0000000..43392ce
--- /dev/null
+++ b/lerp/lerp64_test.go
@@ -0,0 +1,280 @@
+package lerp_test
+
+import (
+ "testing"
+
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+func TestLerp64(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
+ min, max := 0, 10
+ if actual := lerp.NewLerp64(min, max); actual == nil {
+ t.Fatalf("Lerp64[%v, %v] NewLerp64: expected[non-nil] actual[nil]", min, max)
+ }
+ })
+
+ t.Run("Lerp", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := min, l.Lerp(0.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] Lerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := max, l.Lerp(1.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] Lerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // Lerp() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := -2*max, l.Lerp(-2.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] Lerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := 2*max, l.Lerp(2.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] Lerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("ClampedLerp", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := min, l.ClampedLerp(0.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := max, l.ClampedLerp(1.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := min, l.ClampedLerp(-2.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := max, l.ClampedLerp(2.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("LerpRound", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := min, l.LerpRound(0.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] LerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := max, l.LerpRound(1.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] LerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // LerpRound() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := -2*max+1, l.LerpRound(-2.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] LerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := 2*max, l.LerpRound(2.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] LerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("ClampedLerpRound", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := min, l.ClampedLerpRound(0.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedLerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := max, l.ClampedLerpRound(1.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedLerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // ClampedLerpRound() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := min, l.ClampedLerpRound(-2.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedLerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := max, l.ClampedLerpRound(2.0); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedLerpRound: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("InverseLerp", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(0.0), l.InverseLerp(min); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Zero", func(t *testing.T) {
+ min, max := 5, 5
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(0.0), l.InverseLerp(max); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(1.0), l.InverseLerp(max); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // InverseLerp() will work as an inverse linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(-0.2), l.InverseLerp(-2); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(1.2), l.InverseLerp(12); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] InverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("ClampedInverseLerp", func(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(0.0), l.ClampedInverseLerp(min); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Zero", func(t *testing.T) {
+ min, max := 5, 5
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(0.0), l.ClampedInverseLerp(max); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(1.0), l.ClampedInverseLerp(max); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(0.0), l.ClampedInverseLerp(-2); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := float64(1.0), l.ClampedInverseLerp(12); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] ClampedInverseLerp: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("OutputMinimum", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := min, l.OutputMinimum(); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] OutputMinimum: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+
+ t.Run("OutputMaximum", func(t *testing.T) {
+ min, max := 0, 10
+ l := lerp.NewLerp64(min, max)
+ if expected, actual := max, l.OutputMaximum(); actual != expected {
+ t.Fatalf("Lerp64[%v, %v] OutputMaximum: expected[%v] actual[%v]", min, max, expected, actual)
+ }
+ })
+}
diff --git a/lerp/remap.go b/lerp/remap.go
new file mode 100644
index 0000000..047c6e5
--- /dev/null
+++ b/lerp/remap.go
@@ -0,0 +1,13 @@
+package lerp
+
+type Remapper[TIn, TOut Lerpable, TFloat Float] interface {
+ Remap(in TIn) TOut
+ Unmap(out TOut) TIn
+ InputMinimum() TIn
+ InputMaximum() TIn
+ OutputMinimum() TOut
+ OutputMaximum() TOut
+}
+
+type Remapper32[TIn, TOut Lerpable] Remapper[TIn, TOut, float32]
+type Remapper64[TIn, TOut Lerpable] Remapper[TIn, TOut, float64]
diff --git a/lerp/remap32.go b/lerp/remap32.go
new file mode 100644
index 0000000..1f91515
--- /dev/null
+++ b/lerp/remap32.go
@@ -0,0 +1,39 @@
+package lerp
+
+type remap32[TIn, TOut Lerpable] struct {
+ inLerp Lerper32[TIn]
+ outLerp Lerper32[TOut]
+}
+
+func NewRemap32[TIn, TOut Lerpable](inMin, inMax TIn, outMin, outMax TOut) Remapper32[TIn, TOut] {
+ return remap32[TIn, TOut]{
+ inLerp: NewLerp32(inMin, inMax),
+ outLerp: NewLerp32(outMin, outMax),
+ }
+}
+
+func (r remap32[TIn, TOut]) Remap(value TIn) TOut {
+ t := r.inLerp.InverseLerp(value)
+ return r.outLerp.Lerp(t)
+}
+
+func (r remap32[TIn, TOut]) Unmap(value TOut) TIn {
+ t := r.outLerp.InverseLerp(value)
+ return r.inLerp.Lerp(t)
+}
+
+func (r remap32[TIn, TOut]) InputMinimum() TIn {
+ return r.inLerp.OutputMinimum()
+}
+
+func (r remap32[TIn, TOut]) InputMaximum() TIn {
+ return r.inLerp.OutputMaximum()
+}
+
+func (r remap32[TIn, TOut]) OutputMinimum() TOut {
+ return r.outLerp.OutputMinimum()
+}
+
+func (r remap32[TIn, TOut]) OutputMaximum() TOut {
+ return r.outLerp.OutputMaximum()
+}
diff --git a/lerp/remap32_test.go b/lerp/remap32_test.go
new file mode 100644
index 0000000..18246c8
--- /dev/null
+++ b/lerp/remap32_test.go
@@ -0,0 +1,160 @@
+package lerp_test
+
+import (
+ "math"
+ "testing"
+
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+func TestRemap32(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ if actual := lerp.NewRemap32(inMin, inMax, outMin, outMax); actual == nil {
+ t.Fatalf("Remap32[%v, %v, %v, %v] NewRemap32: expected[non-nil] actual[nil]", inMin, inMax, outMin, outMax)
+ }
+ })
+
+ t.Run("Remap", func(t *testing.T) {
+ t.Run("ZeroRange", func(t *testing.T) {
+ inMin, inMax := 10, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := outMin, l.Remap(inMin); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := outMin, l.Remap(inMin); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := outMax, l.Remap(inMax); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // Remap() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := float32(-4.39822971502571), l.Remap(-2); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ // we get a slightly different number than expectation due to floating-point accuracy.
+ if expected, actual := float32(4.3982306), l.Remap(12); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("Unmap", func(t *testing.T) {
+ t.Run("ZeroRange", func(t *testing.T) {
+ inMin, inMax := 10, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := inMin, l.Unmap(outMin); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := inMin, l.Unmap(outMin); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := inMax, l.Unmap(outMax); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // Unmap() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ // one would correctly assume that the expected value would be -2,
+ // but due to floating point error, it's really -1.9999996, which
+ // truncates down to -1
+ if expected, actual := -1, l.Unmap(float32(-4.39822971502571)); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := 12, l.Unmap(float32(4.39822971502571)); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("InputMinimum", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := inMin, l.InputMinimum(); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] InputMinimum: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("InputMaximum", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := inMax, l.InputMaximum(); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] InputMaximum: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("OutputMinimum", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := outMin, l.OutputMinimum(); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] OutputMinimum: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("OutputMaximum", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float32(-math.Pi), float32(math.Pi)
+ l := lerp.NewRemap32(inMin, inMax, outMin, outMax)
+ if expected, actual := outMax, l.OutputMaximum(); actual != expected {
+ t.Fatalf("Remap32[%v, %v, %v, %v] OutputMaximum: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+}
diff --git a/lerp/remap64.go b/lerp/remap64.go
new file mode 100644
index 0000000..69b6907
--- /dev/null
+++ b/lerp/remap64.go
@@ -0,0 +1,39 @@
+package lerp
+
+type remap64[TIn, TOut Lerpable] struct {
+ inLerp Lerper64[TIn]
+ outLerp Lerper64[TOut]
+}
+
+func NewRemap64[TIn, TOut Lerpable](inMin, inMax TIn, outMin, outMax TOut) Remapper64[TIn, TOut] {
+ return remap64[TIn, TOut]{
+ inLerp: NewLerp64(inMin, inMax),
+ outLerp: NewLerp64(outMin, outMax),
+ }
+}
+
+func (r remap64[TIn, TOut]) Remap(value TIn) TOut {
+ t := r.inLerp.InverseLerp(value)
+ return r.outLerp.Lerp(t)
+}
+
+func (r remap64[TIn, TOut]) Unmap(value TOut) TIn {
+ t := r.outLerp.InverseLerp(value)
+ return r.inLerp.Lerp(t)
+}
+
+func (r remap64[TIn, TOut]) InputMinimum() TIn {
+ return r.inLerp.OutputMinimum()
+}
+
+func (r remap64[TIn, TOut]) InputMaximum() TIn {
+ return r.inLerp.OutputMaximum()
+}
+
+func (r remap64[TIn, TOut]) OutputMinimum() TOut {
+ return r.outLerp.OutputMinimum()
+}
+
+func (r remap64[TIn, TOut]) OutputMaximum() TOut {
+ return r.outLerp.OutputMaximum()
+}
diff --git a/lerp/remap64_test.go b/lerp/remap64_test.go
new file mode 100644
index 0000000..f23a3db
--- /dev/null
+++ b/lerp/remap64_test.go
@@ -0,0 +1,156 @@
+package lerp_test
+
+import (
+ "math"
+ "testing"
+
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+func TestRemap64(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ if actual := lerp.NewRemap64(inMin, inMax, outMin, outMax); actual == nil {
+ t.Fatalf("Remap64[%v, %v, %v, %v] NewRemap64: expected[non-nil] actual[nil]", inMin, inMax, outMin, outMax)
+ }
+ })
+
+ t.Run("Remap", func(t *testing.T) {
+ t.Run("ZeroRange", func(t *testing.T) {
+ inMin, inMax := 10, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := outMin, l.Remap(inMin); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := outMin, l.Remap(inMin); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := outMax, l.Remap(inMax); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // Remap() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := float64(-4.39822971502571), l.Remap(-2); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := float64(4.39822971502571), l.Remap(12); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Remap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("Unmap", func(t *testing.T) {
+ t.Run("ZeroRange", func(t *testing.T) {
+ inMin, inMax := 10, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := inMin, l.Unmap(outMin); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := inMin, l.Unmap(outMin); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := inMax, l.Unmap(outMax); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ // Unmap() will work as a linear extrapolator when operating out of range
+ t.Run("BelowMin", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := -2, l.Unmap(float64(-4.39822971502571)); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := 12, l.Unmap(float64(4.39822971502571)); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] Unmap: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("InputMinimum", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := inMin, l.InputMinimum(); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] InputMinimum: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("InputMaximum", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := inMax, l.InputMaximum(); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] InputMaximum: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("OutputMinimum", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := outMin, l.OutputMinimum(); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] OutputMinimum: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+
+ t.Run("OutputMaximum", func(t *testing.T) {
+ inMin, inMax := 0, 10
+ outMin, outMax := float64(-math.Pi), float64(math.Pi)
+ l := lerp.NewRemap64(inMin, inMax, outMin, outMax)
+ if expected, actual := outMax, l.OutputMaximum(); actual != expected {
+ t.Fatalf("Remap64[%v, %v, %v, %v] OutputMaximum: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual)
+ }
+ })
+}
diff --git a/outputer.go b/outputer.go
deleted file mode 100644
index 4512bad..0000000
--- a/outputer.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package europi
-
-import (
- "log"
- "machine"
-)
-
-const (
- // Manually calibrated to best match expected voltages. Additional info:
- // https://github.com/Allen-Synthesis/EuroPi/blob/main/software/programming_instructions.md#calibrate-the-module
- CalibratedOffset = 0
- // The default PWM Top of MaxUint16 caused noisy output. Dropping this down to a 8bit value resulted in much smoother cv output.
- CalibratedTop = 0xff - CalibratedOffset
-)
-
-// We need a rather high frequency to achieve a stable cv ouput, which means we need a rather low duty cycle period.
-// Set a period of 500ns.
-var defaultPeriod uint64 = 500
-
-// PWMer is an interface for interacting with a machine.pwmGroup
-type PWMer interface {
- Configure(config machine.PWMConfig) error
- Channel(pin machine.Pin) (channel uint8, err error)
- Top() uint32
- SetTop(top uint32)
- Get(channel uint8) (value uint32)
- Set(channel uint8, value uint32)
- SetPeriod(period uint64) error
-}
-
-// Outputer is an interface for interacting with the cv output jacks.
-type Outputer interface {
- Get() (value uint32)
- Voltage(v float32)
- On()
- Off()
-}
-
-// Outputer is struct for interacting with the cv output jacks.
-type Output struct {
- pwm PWMer
- pin machine.Pin
- ch uint8
-}
-
-// NewOutput returns a new Output struct.
-func NewOutput(pin machine.Pin, pwm PWMer) *Output {
- err := pwm.Configure(machine.PWMConfig{
- Period: defaultPeriod,
- })
- if err != nil {
- log.Fatal("pwm Configure error: ", err.Error())
- }
-
- pwm.SetTop(CalibratedTop)
-
- ch, err := pwm.Channel(pin)
- if err != nil {
- log.Fatal("pwm Channel error: ", err.Error())
- }
-
- return &Output{pwm, pin, ch}
-}
-
-// Get returns the current set voltage in the range of 0 to pwm.Top().
-func (o *Output) Get() uint32 {
- return o.pwm.Get(o.ch)
-}
-
-// Voltage sets the current output voltage within a range of 0.0 to 10.0.
-func (o *Output) Voltage(v float32) {
- v = Clamp(v, MinVoltage, MaxVoltage)
- invertedCv := (v / MaxVoltage) * float32(o.pwm.Top())
- // cv := (float32(o.pwm.Top()) - invertedCv) - CalibratedOffset
- cv := float32(invertedCv) - CalibratedOffset
- o.pwm.Set(o.ch, uint32(cv))
-}
-
-// On sets the current voltage high at 10.0v.
-func (o *Output) On() {
- o.pwm.Set(o.ch, o.pwm.Top())
-}
-
-// Off sets the current voltage low at 0.0v.
-func (o *Output) Off() {
- o.pwm.Set(o.ch, 0)
-}
diff --git a/quantizer/mode.go b/quantizer/mode.go
new file mode 100644
index 0000000..a66f0a7
--- /dev/null
+++ b/quantizer/mode.go
@@ -0,0 +1,9 @@
+package quantizer
+
+// Mode specifies the kind of Quantizer function to be used.
+type Mode int
+
+const (
+ ModeRound = Mode(iota)
+ ModeTrunc
+)
diff --git a/quantizer/quantizer.go b/quantizer/quantizer.go
new file mode 100644
index 0000000..8fae0f5
--- /dev/null
+++ b/quantizer/quantizer.go
@@ -0,0 +1,20 @@
+package quantizer
+
+// Quantizer specifies a quantization interface.
+type Quantizer[T any] interface {
+ QuantizeToIndex(in float32, length int) int
+ QuantizeToValue(in float32, list []T) T
+}
+
+// New creates a quantizer of the specified `mode`.
+func New[T any](mode Mode) Quantizer[T] {
+ switch mode {
+ case ModeRound:
+ return &Round[T]{}
+ case ModeTrunc:
+ return &Trunc[T]{}
+ default:
+ // unsupported mode
+ return nil
+ }
+}
diff --git a/quantizer/quantizer_round.go b/quantizer/quantizer_round.go
new file mode 100644
index 0000000..36c902f
--- /dev/null
+++ b/quantizer/quantizer_round.go
@@ -0,0 +1,35 @@
+package quantizer
+
+import (
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+// Round is a rounding-style quantizer
+type Round[T any] struct{}
+
+// QuantizeToIndex takes a normalized input value and a length value, then provides
+// a return value between 0 and (length - 1), inclusive.
+//
+// A return value of -1 means that the length parameter was 0 (a value that cannot be quantized over successfully).
+func (Round[T]) QuantizeToIndex(in float32, length int) int {
+ if length == 0 {
+ return -1
+ }
+
+ return lerp.NewLerp32(0, length-1).ClampedLerpRound(in)
+}
+
+// QuantizeToValue takes a normalized input value and a list of values, then provides
+// a return value chosen from the provided list of values.
+//
+// A return value of the zeroish equivalent of the value means that the list parameter
+// was empty (this situation does not lend well to quantization).
+func (q Round[T]) QuantizeToValue(in float32, list []T) T {
+ idx := q.QuantizeToIndex(in, len(list))
+ if idx == -1 {
+ var empty T
+ return empty
+ }
+
+ return list[idx]
+}
diff --git a/quantizer/quantizer_round_test.go b/quantizer/quantizer_round_test.go
new file mode 100644
index 0000000..1c29c2b
--- /dev/null
+++ b/quantizer/quantizer_round_test.go
@@ -0,0 +1,111 @@
+package quantizer_test
+
+import (
+ "testing"
+
+ "github.com/awonak/EuroPiGo/quantizer"
+)
+
+func TestRound(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
+ if actual := quantizer.New[int](quantizer.ModeRound); actual == nil {
+ t.Fatalf("Quantizer[%v] New: expected[non-nil] actual[nil]", quantizer.ModeRound)
+ }
+ })
+
+ t.Run("QuantizeToIndex", func(t *testing.T) {
+ q := quantizer.New[int](quantizer.ModeRound)
+
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := float32(0)
+ length := 10
+ if expected, actual := 0, q.QuantizeToIndex(min, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeRound, min, length, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := float32(1)
+ length := 10
+ if expected, actual := length-1, q.QuantizeToIndex(max, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeRound, max, length, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := float32(-1)
+ length := 10
+ if expected, actual := 0, q.QuantizeToIndex(belowMin, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeRound, belowMin, length, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := float32(2)
+ length := 10
+ if expected, actual := length-1, q.QuantizeToIndex(aboveMax, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeRound, aboveMax, length, expected, actual)
+ }
+ })
+
+ t.Run("EmptySet", func(t *testing.T) {
+ emptySet := float32(0.5)
+ length := 0
+ if expected, actual := -1, q.QuantizeToIndex(emptySet, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeRound, emptySet, length, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("QuantizeToValue", func(t *testing.T) {
+ q := quantizer.New[int](quantizer.ModeRound)
+
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := float32(0)
+ list := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ if expected, actual := list[0], q.QuantizeToValue(min, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeRound, min, list, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := float32(1)
+ list := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ if expected, actual := list[len(list)-1], q.QuantizeToValue(max, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeRound, max, list, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := float32(-1)
+ list := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ if expected, actual := list[0], q.QuantizeToValue(belowMin, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeRound, belowMin, list, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := float32(2)
+ list := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ if expected, actual := list[len(list)-1], q.QuantizeToValue(aboveMax, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeRound, aboveMax, list, expected, actual)
+ }
+ })
+
+ t.Run("EmptySet", func(t *testing.T) {
+ emptySet := float32(0.5)
+ var list []int
+ if expected, actual := 0, q.QuantizeToValue(emptySet, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeRound, emptySet, list, expected, actual)
+ }
+ })
+ })
+ })
+}
diff --git a/quantizer/quantizer_test.go b/quantizer/quantizer_test.go
new file mode 100644
index 0000000..4e76433
--- /dev/null
+++ b/quantizer/quantizer_test.go
@@ -0,0 +1,17 @@
+package quantizer_test
+
+import (
+ "math"
+ "testing"
+
+ "github.com/awonak/EuroPiGo/quantizer"
+)
+
+func TestNew(t *testing.T) {
+ t.Run("Invalid", func(t *testing.T) {
+ mode := quantizer.Mode(math.MinInt)
+ if actual := quantizer.New[int](mode); actual != nil {
+ t.Fatalf("Quantizer[%v] New: expected[nil] actual[non-nil]", mode)
+ }
+ })
+}
diff --git a/quantizer/quantizer_trunc.go b/quantizer/quantizer_trunc.go
new file mode 100644
index 0000000..539a095
--- /dev/null
+++ b/quantizer/quantizer_trunc.go
@@ -0,0 +1,36 @@
+package quantizer
+
+import (
+ "github.com/awonak/EuroPiGo/lerp"
+)
+
+// Trunc is a truncating-style quantizer.
+// That is, it performs a mathematical `floor` if positive and `ceiling` if negative.
+type Trunc[T any] struct{}
+
+// QuantizeToIndex takes a normalized input value and a length value, then provides
+// a return value between 0 and (length - 1), inclusive.
+//
+// A return value of -1 means that the length parameter was 0 (a value that cannot be quantized over successfully).
+func (Trunc[T]) QuantizeToIndex(in float32, length int) int {
+ if length == 0 {
+ return -1
+ }
+
+ return lerp.NewLerp32(0, length-1).ClampedLerp(in)
+}
+
+// QuantizeToValue takes a normalized input value and a list of values, then provides
+// a return value chosen from the provided list of values.
+//
+// A return value of the zeroish equivalent of the value means that the list parameter
+// was empty (this situation does not lend well to quantization).
+func (q Trunc[T]) QuantizeToValue(in float32, list []T) T {
+ idx := q.QuantizeToIndex(in, len(list))
+ if idx == -1 {
+ var empty T
+ return empty
+ }
+
+ return list[idx]
+}
diff --git a/quantizer/quantizer_trunc_test.go b/quantizer/quantizer_trunc_test.go
new file mode 100644
index 0000000..05af357
--- /dev/null
+++ b/quantizer/quantizer_trunc_test.go
@@ -0,0 +1,111 @@
+package quantizer_test
+
+import (
+ "testing"
+
+ "github.com/awonak/EuroPiGo/quantizer"
+)
+
+func TestTrunc(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
+ if actual := quantizer.New[int](quantizer.ModeTrunc); actual == nil {
+ t.Fatalf("Quantizer[%v] New: expected[non-nil] actual[nil]", quantizer.ModeTrunc)
+ }
+ })
+
+ t.Run("QuantizeToIndex", func(t *testing.T) {
+ q := quantizer.New[int](quantizer.ModeTrunc)
+
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := float32(0)
+ length := 10
+ if expected, actual := 0, q.QuantizeToIndex(min, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeTrunc, min, length, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := float32(1)
+ length := 10
+ if expected, actual := length-1, q.QuantizeToIndex(max, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeTrunc, max, length, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := float32(-1)
+ length := 10
+ if expected, actual := 0, q.QuantizeToIndex(belowMin, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeTrunc, belowMin, length, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := float32(2)
+ length := 10
+ if expected, actual := length-1, q.QuantizeToIndex(aboveMax, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeTrunc, aboveMax, length, expected, actual)
+ }
+ })
+
+ t.Run("EmptySet", func(t *testing.T) {
+ emptySet := float32(0.5)
+ length := 0
+ if expected, actual := -1, q.QuantizeToIndex(emptySet, length); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToIndex(%v, %v): expected[%v] actual[%v]", quantizer.ModeTrunc, emptySet, length, expected, actual)
+ }
+ })
+ })
+ })
+
+ t.Run("QuantizeToValue", func(t *testing.T) {
+ q := quantizer.New[int](quantizer.ModeTrunc)
+
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := float32(0)
+ list := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ if expected, actual := list[0], q.QuantizeToValue(min, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeTrunc, min, list, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := float32(1)
+ list := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ if expected, actual := list[len(list)-1], q.QuantizeToValue(max, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeTrunc, max, list, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := float32(-1)
+ list := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ if expected, actual := list[0], q.QuantizeToValue(belowMin, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeTrunc, belowMin, list, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := float32(2)
+ list := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ if expected, actual := list[len(list)-1], q.QuantizeToValue(aboveMax, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeTrunc, aboveMax, list, expected, actual)
+ }
+ })
+
+ t.Run("EmptySet", func(t *testing.T) {
+ emptySet := float32(0.5)
+ var list []int
+ if expected, actual := 0, q.QuantizeToValue(emptySet, list); actual != expected {
+ t.Fatalf("Quantizer[%v] QuantizeToValue(%v, %T): expected[%v] actual[%v]", quantizer.ModeTrunc, emptySet, list, expected, actual)
+ }
+ })
+ })
+ })
+}
diff --git a/scripts/diagnostics/diagnostics.go b/scripts/diagnostics/diagnostics.go
deleted file mode 100644
index f6c1043..0000000
--- a/scripts/diagnostics/diagnostics.go
+++ /dev/null
@@ -1,79 +0,0 @@
-// Diagnostics is a script for demonstrating all main interactions with the EuroPiGo firmware.
-package main
-
-import (
- "fmt"
- "machine"
-
- "tinygo.org/x/tinydraw"
-
- europi "github.com/awonak/EuroPiGo"
-)
-
-type MyApp struct {
- knobsDisplayPercent bool
- prevK1 uint16
- prevK2 uint16
- staticCv int
- prevStaticCv int
-}
-
-func main() {
-
- myApp := MyApp{
- staticCv: 5,
- }
-
- e := europi.New()
-
- // Demonstrate adding a IRQ handler to B1 and B2.
- e.B1.Handler(func(p machine.Pin) {
- myApp.knobsDisplayPercent = !myApp.knobsDisplayPercent
- })
-
- e.B2.Handler(func(p machine.Pin) {
- myApp.staticCv = (myApp.staticCv + 1) % europi.MaxVoltage
- })
-
- for {
- e.Display.ClearBuffer()
-
- // Highlight the border of the oled display.
- tinydraw.Rectangle(e.Display, 0, 0, 128, 32, europi.White)
-
- // Display analog and digital input values.
- inputText := fmt.Sprintf("din: %5v ain: %2.2f ", e.DI.Value(), e.AI.Percent())
- e.Display.WriteLine(inputText, 3, 8)
-
- // Display knob values based on app state.
- var knobText string
- if myApp.knobsDisplayPercent {
- knobText = fmt.Sprintf("K1: %0.2f K2: %0.2f", e.K1.Percent(), e.K2.Percent())
- } else {
- knobText = fmt.Sprintf("K1: %2d K2: %2d", e.K1.Range(100), e.K2.Range(100))
- }
- e.Display.WriteLine(knobText, 3, 18)
-
- // Show current button press state.
- e.Display.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", e.B1.Value(), e.B2.Value()), 3, 28)
-
- e.Display.Display()
-
- // Set voltage values for the 6 CV outputs.
- if e.K1.Range(1<<12) != myApp.prevK1 {
- e.CV1.Voltage(e.K1.ReadVoltage())
- e.CV4.Voltage(europi.MaxVoltage - e.K1.ReadVoltage())
- myApp.prevK1 = e.K1.Range(1 << 12)
- }
- if e.K2.Range(1<<12) != myApp.prevK2 {
- e.CV2.Voltage(e.K2.ReadVoltage())
- e.CV5.Voltage(europi.MaxVoltage - e.K2.ReadVoltage())
- myApp.prevK2 = e.K2.Range(1 << 12)
- }
- e.CV3.On()
- if myApp.staticCv != myApp.prevStaticCv {
- e.CV6.Voltage(float32(myApp.staticCv))
- myApp.prevStaticCv = myApp.staticCv
- }
- }
-}
diff --git a/units/bipolarcv.go b/units/bipolarcv.go
new file mode 100644
index 0000000..8cde411
--- /dev/null
+++ b/units/bipolarcv.go
@@ -0,0 +1,24 @@
+package units
+
+import "github.com/awonak/EuroPiGo/clamp"
+
+// BipolarCV is a normalized representation [-1.0 .. 1.0] of a Control Voltage [-5.0 .. 5.0] value.
+type BipolarCV float32
+
+// ToVolts converts a (normalized) BipolarCV value to a value between -5.0 and 5.0 volts
+func (c BipolarCV) ToVolts() float32 {
+ return c.ToFloat32() * 5.0
+}
+
+// ToCV converts a (normalized) BipolarCV value to a (normalized) CV value and a sign bit
+func (c BipolarCV) ToCV() (cv CV, sign int) {
+ if c < 0.0 {
+ return CV(-c.ToFloat32()), -1
+ }
+ return CV(c.ToFloat32()), 1
+}
+
+// ToFloat32 returns a (normalized) BipolarCV value to its floating point representation [-1.0 .. 1.0]
+func (c BipolarCV) ToFloat32() float32 {
+ return clamp.Clamp(float32(c), -1.0, 1.0)
+}
diff --git a/units/bipolarcv_test.go b/units/bipolarcv_test.go
new file mode 100644
index 0000000..67813c8
--- /dev/null
+++ b/units/bipolarcv_test.go
@@ -0,0 +1,205 @@
+package units_test
+
+import (
+ "math"
+ "testing"
+
+ "github.com/awonak/EuroPiGo/units"
+)
+
+func TestBipolarCVToVolts(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := units.BipolarCV(-1.0)
+ if expected, actual := float32(-5.0), min.ToVolts(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToVolts: expected[%f] actual[%f]", min, expected, actual)
+ }
+ })
+
+ t.Run("Zero", func(t *testing.T) {
+ zero := units.BipolarCV(0.0)
+ if expected, actual := float32(0.0), zero.ToVolts(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToVolts: expected[%f] actual[%f]", zero, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := units.BipolarCV(1.0)
+ if expected, actual := float32(5.0), max.ToVolts(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToVolts: expected[%f] actual[%f]", max, expected, actual)
+ }
+ })
+ })
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := units.BipolarCV(-2.0)
+ if expected, actual := float32(-5.0), belowMin.ToVolts(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToVolts: expected[%f] actual[%f]", belowMin, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := units.BipolarCV(2.0)
+ if expected, actual := float32(5.0), aboveMax.ToVolts(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToVolts: expected[%f] actual[%f]", aboveMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("NaN", func(t *testing.T) {
+ nan := units.BipolarCV(math.NaN())
+ if actual := nan.ToVolts(); !math.IsNaN(float64(actual)) {
+ t.Fatalf("BipolarCV[%v] ToVolts: expected[%f] actual[%f]", nan, math.NaN(), actual)
+ }
+ })
+
+ t.Run("Inf", func(t *testing.T) {
+ t.Run("Neg", func(t *testing.T) {
+ negInf := units.BipolarCV(math.Inf(-1))
+ if expected, actual := float32(-5.0), negInf.ToVolts(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToVolts: expected[%f] actual[%f]", negInf, expected, actual)
+ }
+ })
+
+ t.Run("Pos", func(t *testing.T) {
+ posInf := units.BipolarCV(math.Inf(1))
+ if expected, actual := float32(5.0), posInf.ToVolts(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToVolts: expected[%f] actual[%f]", posInf, expected, actual)
+ }
+ })
+ })
+}
+
+func TestBipolarCVToCV(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := units.BipolarCV(-1.0)
+ expectedMin, expectedMinSign := units.CV(1.0), -1
+ if actual, actualSign := min.ToCV(); actual != expectedMin || actualSign != expectedMinSign {
+ t.Fatalf("BipolarCV[%v] ToCV: expected[%f sign(%d)] actual[%f sign(%d)]", min, expectedMin, expectedMinSign, actual, actualSign)
+ }
+ })
+
+ t.Run("Zero", func(t *testing.T) {
+ zero := units.BipolarCV(0.0)
+ expectedZero, expectedZeroSign := units.CV(0.0), 1
+ if actual, actualSign := zero.ToCV(); actual != expectedZero || actualSign != expectedZeroSign {
+ t.Fatalf("BipolarCV[%v] ToCV: expected[%f sign(%d)] actual[%f sign(%d)]", zero, expectedZero, expectedZeroSign, actual, actualSign)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := units.BipolarCV(1.0)
+ expectedMax, expectedMaxSign := units.CV(1.0), 1
+ if actual, actualSign := max.ToCV(); actual != expectedMax || actualSign != expectedMaxSign {
+ t.Fatalf("BipolarCV[%v] ToCV: expected[%f sign(%d)] actual[%f sign(%d)]", max, expectedMax, expectedMaxSign, actual, actualSign)
+ }
+ })
+ })
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := units.BipolarCV(-2.0)
+ expectedBelowMin, expectedBelowMinSign := units.CV(1.0), -1
+ if actual, actualSign := belowMin.ToCV(); actual != expectedBelowMin || actualSign != expectedBelowMinSign {
+ t.Fatalf("BipolarCV[%v] ToCV: expected[%f sign(%d)] actual[%f sign(%d)]", belowMin, expectedBelowMin, expectedBelowMinSign, actual, actualSign)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := units.BipolarCV(2.0)
+ expectedAboveMax, expectedAboveMaxSign := units.CV(1.0), 1
+ if actual, actualSign := aboveMax.ToCV(); actual != expectedAboveMax || actualSign != expectedAboveMaxSign {
+ t.Fatalf("BipolarCV[%v] ToCV: expected[%f sign(%d)] actual[%f sign(%d)]", aboveMax, expectedAboveMax, expectedAboveMaxSign, actual, actualSign)
+ }
+ })
+ })
+
+ t.Run("NaN", func(t *testing.T) {
+ nan := units.BipolarCV(math.NaN())
+ expectedNanSign := 1
+ if actual, actualSign := nan.ToCV(); !math.IsNaN(float64(actual)) || actualSign != expectedNanSign {
+ t.Fatalf("BipolarCV[%v] ToCV: expected[%f sign(%d)] actual[%f sign(%d)]", nan, math.NaN(), expectedNanSign, actual, actualSign)
+ }
+ })
+
+ t.Run("Inf", func(t *testing.T) {
+ t.Run("Neg", func(t *testing.T) {
+ negInf := units.BipolarCV(math.Inf(-1))
+ expectedNegInf, expectedNegInfSign := units.CV(1.0), -1
+ if actual, actualSign := negInf.ToCV(); actual != expectedNegInf || actualSign != expectedNegInfSign {
+ t.Fatalf("BipolarCV[%v] ToCV: expected[%f sign(%d)] actual[%f sign(%d)]", negInf, expectedNegInf, expectedNegInfSign, actual, actualSign)
+ }
+ })
+
+ t.Run("Pos", func(t *testing.T) {
+ posInf := units.BipolarCV(math.Inf(1))
+ expectedPosInf, expectedPosInfSign := units.CV(1.0), 1
+ if actual, actualSign := posInf.ToCV(); actual != expectedPosInf || actualSign != expectedPosInfSign {
+ t.Fatalf("BipolarCV[%v] ToCV: expected[%f sign(%d)] actual[%f sign(%d)]", posInf, expectedPosInf, expectedPosInfSign, actual, actualSign)
+ }
+ })
+ })
+}
+
+func TestBipolarCVToFloat32(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := units.BipolarCV(-1.0)
+ if expected, actual := float32(-1.0), min.ToFloat32(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToFloat32: expected[%f] actual[%f]", min, expected, actual)
+ }
+ })
+
+ t.Run("Zero", func(t *testing.T) {
+ zero := units.BipolarCV(0.0)
+ if expected, actual := float32(0.0), zero.ToFloat32(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToFloat32: expected[%f] actual[%f]", zero, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := units.BipolarCV(1.0)
+ if expected, actual := float32(1.0), max.ToFloat32(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToFloat32: expected[%f] actual[%f]", max, expected, actual)
+ }
+ })
+ })
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := units.BipolarCV(-2.0)
+ if expected, actual := float32(-1.0), belowMin.ToFloat32(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToFloat32: expected[%f] actual[%f]", belowMin, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := units.BipolarCV(2.0)
+ if expected, actual := float32(1.0), aboveMax.ToFloat32(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToFloat32: expected[%f] actual[%f]", aboveMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("NaN", func(t *testing.T) {
+ nan := units.BipolarCV(math.NaN())
+ if actual := nan.ToFloat32(); !math.IsNaN(float64(actual)) {
+ t.Fatalf("BipolarCV[%v] ToFloat32: expected[%f] actual[%f]", nan, math.NaN(), actual)
+ }
+ })
+
+ t.Run("Inf", func(t *testing.T) {
+ t.Run("Neg", func(t *testing.T) {
+ negInf := units.BipolarCV(math.Inf(-1))
+ if expected, actual := float32(-1.0), negInf.ToFloat32(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToFloat32: expected[%f] actual[%f]", negInf, expected, actual)
+ }
+ })
+
+ t.Run("Pos", func(t *testing.T) {
+ posInf := units.BipolarCV(math.Inf(1))
+ if expected, actual := float32(1.0), posInf.ToFloat32(); actual != expected {
+ t.Fatalf("BipolarCV[%v] ToFloat32: expected[%f] actual[%f]", posInf, expected, actual)
+ }
+ })
+ })
+}
diff --git a/units/cv.go b/units/cv.go
new file mode 100644
index 0000000..b23c687
--- /dev/null
+++ b/units/cv.go
@@ -0,0 +1,25 @@
+package units
+
+import "github.com/awonak/EuroPiGo/clamp"
+
+// CV is a normalized representation [0.0 .. 1.0] of a Control Voltage [0.0 .. 5.0] value.
+type CV float32
+
+// ToVolts converts a (normalized) CV value to a value between 0.0 and 5.0 volts
+func (c CV) ToVolts() float32 {
+ return c.ToFloat32() * 5.0
+}
+
+// ToBipolarCV converts a (normalized) CV value to a (normalized) BipolarCV value
+func (c CV) ToBipolarCV(sign int) BipolarCV {
+ bc := BipolarCV(c.ToFloat32())
+ if sign < 0 {
+ return -bc
+ }
+ return bc
+}
+
+// ToFloat32 returns a (normalized) CV value to its floating point representation [0.0 .. 1.0]
+func (c CV) ToFloat32() float32 {
+ return clamp.Clamp(float32(c), 0.0, 1.0)
+}
diff --git a/units/cv_test.go b/units/cv_test.go
new file mode 100644
index 0000000..6ed525e
--- /dev/null
+++ b/units/cv_test.go
@@ -0,0 +1,185 @@
+package units_test
+
+import (
+ "math"
+ "testing"
+
+ "github.com/awonak/EuroPiGo/units"
+)
+
+func TestCVToVolts(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := units.CV(-1.0)
+ if expected, actual := float32(0.0), min.ToVolts(); actual != expected {
+ t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", min, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := units.CV(1.0)
+ if expected, actual := float32(5.0), max.ToVolts(); actual != expected {
+ t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", max, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := units.CV(-2.0)
+ if expected, actual := float32(0.0), belowMin.ToVolts(); actual != expected {
+ t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", belowMin, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := units.CV(2.0)
+ if expected, actual := float32(5.0), aboveMax.ToVolts(); actual != expected {
+ t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", aboveMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("NaN", func(t *testing.T) {
+ nan := units.CV(math.NaN())
+ if actual := nan.ToVolts(); !math.IsNaN(float64(actual)) {
+ t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", nan, math.NaN(), actual)
+ }
+ })
+
+ t.Run("Inf", func(t *testing.T) {
+ t.Run("Neg", func(t *testing.T) {
+ negInf := units.CV(math.Inf(-1))
+ if expected, actual := float32(0.0), negInf.ToVolts(); actual != expected {
+ t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", negInf, expected, actual)
+ }
+ })
+
+ t.Run("Pos", func(t *testing.T) {
+ posInf := units.CV(math.Inf(1))
+ if expected, actual := float32(5.0), posInf.ToVolts(); actual != expected {
+ t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", posInf, expected, actual)
+ }
+ })
+ })
+}
+
+func TestCVToBipolarCV(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min, minSign := units.CV(1.0), -1
+ if expected, actual := units.BipolarCV(-1.0), min.ToBipolarCV(minSign); actual != expected {
+ t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", min, minSign, expected, actual)
+ }
+ })
+
+ t.Run("Zero", func(t *testing.T) {
+ zero, zeroSign := units.CV(0.0), 1
+ if expected, actual := units.BipolarCV(0.0), zero.ToBipolarCV(zeroSign); actual != expected {
+ t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", zero, zeroSign, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max, maxSign := units.CV(1.0), 1
+ if expected, actual := units.BipolarCV(1.0), max.ToBipolarCV(maxSign); actual != expected {
+ t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", max, maxSign, expected, actual)
+ }
+ })
+ })
+
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin, belowMinSign := units.CV(2.0), -1
+ if expected, actual := units.BipolarCV(-1.0), belowMin.ToBipolarCV(belowMinSign); actual != expected {
+ t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", belowMin, belowMinSign, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax, aboveMaxSign := units.CV(2.0), 1
+ if expected, actual := units.BipolarCV(1.0), aboveMax.ToBipolarCV(aboveMaxSign); actual != expected {
+ t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", aboveMax, aboveMaxSign, expected, actual)
+ }
+ })
+ })
+
+ t.Run("NaN", func(t *testing.T) {
+ nan, nanSign := units.CV(math.NaN()), 1
+ if actual := nan.ToBipolarCV(nanSign); !math.IsNaN(float64(actual)) {
+ t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", nan, nanSign, nan, actual)
+ }
+ })
+
+ t.Run("Inf", func(t *testing.T) {
+ t.Run("Neg", func(t *testing.T) {
+ negInf, negInfSign := units.CV(math.Inf(1)), -1
+ if expected, actual := units.BipolarCV(-1.0), negInf.ToBipolarCV(negInfSign); actual != expected {
+ t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", negInf, negInfSign, expected, actual)
+ }
+ })
+
+ t.Run("Pos", func(t *testing.T) {
+ posInf, posInfSign := units.CV(math.Inf(1)), 1
+ if expected, actual := units.BipolarCV(1.0), posInf.ToBipolarCV(posInfSign); actual != expected {
+ t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", posInf, posInfSign, expected, actual)
+ }
+ })
+ })
+}
+
+func TestCVToFloat32(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := units.CV(0.0)
+ if expected, actual := float32(0.0), min.ToFloat32(); actual != expected {
+ t.Fatalf("CV[%v] ToFloat32: expected[%f] actual[%f]", min, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := units.CV(1.0)
+ if expected, actual := float32(1.0), max.ToFloat32(); actual != expected {
+ t.Fatalf("CV[%v] ToFloat32: expected[%f] actual[%f]", max, expected, actual)
+ }
+ })
+ })
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := units.CV(-2.0)
+ if expected, actual := float32(0.0), belowMin.ToFloat32(); actual != expected {
+ t.Fatalf("CV[%v] ToFloat32: expected[%f] actual[%f]", belowMin, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := units.CV(2.0)
+ if expected, actual := float32(1.0), aboveMax.ToFloat32(); actual != expected {
+ t.Fatalf("CV[%v] ToFloat32: expected[%f] actual[%f]", aboveMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("NaN", func(t *testing.T) {
+ nan := units.CV(math.NaN())
+ if actual := nan.ToFloat32(); !math.IsNaN(float64(actual)) {
+ t.Fatalf("CV[%v] ToFloat32: expected[%f] actual[%f]", nan, math.NaN(), actual)
+ }
+ })
+
+ t.Run("Inf", func(t *testing.T) {
+ t.Run("Neg", func(t *testing.T) {
+ negInf := units.CV(math.Inf(-1))
+ if expected, actual := float32(0.0), negInf.ToFloat32(); actual != expected {
+ t.Fatalf("CV[%v] ToFloat32: expected[%f] actual[%f]", negInf, expected, actual)
+ }
+ })
+
+ t.Run("Pos", func(t *testing.T) {
+ posInf := units.CV(math.Inf(1))
+ if expected, actual := float32(1.0), posInf.ToFloat32(); actual != expected {
+ t.Fatalf("CV[%v] ToFloat32: expected[%f] actual[%f]", posInf, expected, actual)
+ }
+ })
+ })
+}
diff --git a/units/duration.go b/units/duration.go
new file mode 100644
index 0000000..8520690
--- /dev/null
+++ b/units/duration.go
@@ -0,0 +1,19 @@
+package units
+
+import (
+ "fmt"
+ "time"
+)
+
+func DurationString(dur time.Duration) string {
+ switch {
+ case dur < time.Microsecond:
+ return fmt.Sprint(dur)
+ case dur < time.Millisecond:
+ return fmt.Sprintf("%3.1fus", dur.Seconds()*1_000_000.0)
+ case dur < time.Second:
+ return fmt.Sprintf("%3.1fms", dur.Seconds()*1_000.0)
+ default:
+ return fmt.Sprintf("%3.1fs", dur.Seconds())
+ }
+}
diff --git a/units/duration_test.go b/units/duration_test.go
new file mode 100644
index 0000000..b77a49e
--- /dev/null
+++ b/units/duration_test.go
@@ -0,0 +1,52 @@
+package units_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/awonak/EuroPiGo/units"
+)
+
+func TestDurationString(t *testing.T) {
+ t.Run("Zero", func(t *testing.T) {
+ zero := time.Duration(0)
+ if expected, actual := "0s", units.DurationString(zero); actual != expected {
+ t.Fatalf("Duration[%v] DurationString: expected[%s] actual[%s]", zero, expected, actual)
+ }
+ })
+
+ t.Run("Nanoseconds", func(t *testing.T) {
+ ns := time.Duration(time.Nanosecond * 123)
+ if expected, actual := "123ns", units.DurationString(ns); actual != expected {
+ t.Fatalf("Duration[%v] DurationString: expected[%s] actual[%s]", ns, expected, actual)
+ }
+ })
+
+ t.Run("Microseconds", func(t *testing.T) {
+ us := time.Duration(time.Nanosecond * 12_345)
+ if expected, actual := "12.3us", units.DurationString(us); actual != expected {
+ t.Fatalf("Duration[%v] DurationString: expected[%s] actual[%s]", us, expected, actual)
+ }
+ })
+
+ t.Run("Milliseconds", func(t *testing.T) {
+ ms := time.Duration(time.Microsecond * 12_345)
+ if expected, actual := "12.3ms", units.DurationString(ms); actual != expected {
+ t.Fatalf("Duration[%v] DurationString: expected[%s] actual[%s]", ms, expected, actual)
+ }
+ })
+
+ t.Run("Seconds", func(t *testing.T) {
+ s := time.Duration(time.Millisecond * 12_345)
+ if expected, actual := "12.3s", units.DurationString(s); actual != expected {
+ t.Fatalf("Duration[%v] DurationString: expected[%s] actual[%s]", s, expected, actual)
+ }
+ })
+
+ t.Run("Minutes", func(t *testing.T) {
+ m := time.Duration(time.Millisecond * 754567)
+ if expected, actual := "754.6s", units.DurationString(m); actual != expected {
+ t.Fatalf("Duration[%v] DurationString: expected[%s] actual[%s]", m, expected, actual)
+ }
+ })
+}
diff --git a/units/hertz.go b/units/hertz.go
new file mode 100644
index 0000000..182ac39
--- /dev/null
+++ b/units/hertz.go
@@ -0,0 +1,37 @@
+package units
+
+import (
+ "fmt"
+ "time"
+)
+
+type Hertz float32
+
+func (h Hertz) ToPeriod() time.Duration {
+ if h == 0 {
+ return 0
+ }
+ return time.Duration(float32(time.Second) / float32(h))
+}
+
+func (h Hertz) String() string {
+ switch {
+ case h < 0.000_001:
+ return fmt.Sprintf("%3.1fnHz", h*1_000_000_000.0)
+ case h < 0.001:
+ return fmt.Sprintf("%3.1fuHz", h*1_000_000.0)
+ case h < 1.0:
+ return fmt.Sprintf("%3.1fmHz", h*1_000.0)
+ case h < 1_000.0:
+ return fmt.Sprintf("%3.1fHz", h)
+ case h < 1_000_000.0:
+ return fmt.Sprintf("%3.1fkHz", h/1_000.0)
+ case h < 1_000_000_000.0:
+ return fmt.Sprintf("%3.1fMHz", h/1_000_000.0)
+ case h < 1_000_000_000_000.0:
+ return fmt.Sprintf("%3.1fGHz", h/1_000_000_000.0)
+ default:
+ // use scientific notation
+ return fmt.Sprintf("%3.1gHz", h)
+ }
+}
diff --git a/units/hertz_test.go b/units/hertz_test.go
new file mode 100644
index 0000000..7ef5554
--- /dev/null
+++ b/units/hertz_test.go
@@ -0,0 +1,103 @@
+package units_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/awonak/EuroPiGo/units"
+)
+
+func TestHertzToPeriod(t *testing.T) {
+ t.Run("Zero", func(t *testing.T) {
+ zero := units.Hertz(0)
+ if expected, actual := time.Duration(0), zero.ToPeriod(); actual != expected {
+ t.Fatalf("Hertz[%v] ToPeriod: expected[%s] actual[%s]", zero, expected, actual)
+ }
+ })
+
+ t.Run("Seconds", func(t *testing.T) {
+ s := units.Hertz(0.1)
+ if expected, actual := time.Second*10, s.ToPeriod(); actual != expected {
+ t.Fatalf("Hertz[%v] ToPeriod: expected[%s] actual[%s]", s, expected, actual)
+ }
+ })
+
+ t.Run("Milliseconds", func(t *testing.T) {
+ ms := units.Hertz(100.0)
+ if expected, actual := time.Millisecond*10, ms.ToPeriod(); actual != expected {
+ t.Fatalf("Hertz[%v] ToPeriod: expected[%s] actual[%s]", ms, expected, actual)
+ }
+ })
+
+ t.Run("Microseconds", func(t *testing.T) {
+ us := units.Hertz(100_000.0)
+ if expected, actual := time.Microsecond*10, us.ToPeriod(); actual != expected {
+ t.Fatalf("Hertz[%v] ToPeriod: expected[%s] actual[%s]", us, expected, actual)
+ }
+ })
+
+ t.Run("Nanoseconds", func(t *testing.T) {
+ ns := units.Hertz(100_000_000.0)
+ if expected, actual := time.Nanosecond*10, ns.ToPeriod(); actual != expected {
+ t.Fatalf("Hertz[%v] ToPeriod: expected[%s] actual[%s]", ns, expected, actual)
+ }
+ })
+}
+
+func TestHertzString(t *testing.T) {
+ t.Run("NanoHertz", func(t *testing.T) {
+ nhz := units.Hertz(0.000_000_1)
+ if expected, actual := "100.0nHz", nhz.String(); actual != expected {
+ t.Fatalf("Hertz[%v] String: expected[%s] actual[%s]", nhz, expected, actual)
+ }
+ })
+
+ t.Run("MicroHertz", func(t *testing.T) {
+ uhz := units.Hertz(0.000_1)
+ if expected, actual := "100.0uHz", uhz.String(); actual != expected {
+ t.Fatalf("Hertz[%v] String: expected[%s] actual[%s]", uhz, expected, actual)
+ }
+ })
+
+ t.Run("MilliHertz", func(t *testing.T) {
+ mhz := units.Hertz(0.1)
+ if expected, actual := "100.0mHz", mhz.String(); actual != expected {
+ t.Fatalf("Hertz[%v] String: expected[%s] actual[%s]", mhz, expected, actual)
+ }
+ })
+
+ t.Run("Hertz", func(t *testing.T) {
+ hz := units.Hertz(100.0)
+ if expected, actual := "100.0Hz", hz.String(); actual != expected {
+ t.Fatalf("Hertz[%v] String: expected[%s] actual[%s]", hz, expected, actual)
+ }
+ })
+
+ t.Run("KiloHertz", func(t *testing.T) {
+ khz := units.Hertz(100_000.0)
+ if expected, actual := "100.0kHz", khz.String(); actual != expected {
+ t.Fatalf("Hertz[%v] String: expected[%s] actual[%s]", khz, expected, actual)
+ }
+ })
+
+ t.Run("MegaHertz", func(t *testing.T) {
+ mhz := units.Hertz(100_000_000.0)
+ if expected, actual := "100.0MHz", mhz.String(); actual != expected {
+ t.Fatalf("Hertz[%v] String: expected[%s] actual[%s]", mhz, expected, actual)
+ }
+ })
+
+ t.Run("GigaHertz", func(t *testing.T) {
+ ghz := units.Hertz(100_000_000_000.0)
+ if expected, actual := "100.0GHz", ghz.String(); actual != expected {
+ t.Fatalf("Hertz[%v] String: expected[%s] actual[%s]", ghz, expected, actual)
+ }
+ })
+
+ t.Run("Other", func(t *testing.T) {
+ hz := units.Hertz(100_000_000_000_000.0)
+ if expected, actual := "1e+14Hz", hz.String(); actual != expected {
+ t.Fatalf("Hertz[%v] String: expected[%s] actual[%s]", hz, expected, actual)
+ }
+ })
+}
diff --git a/units/voct.go b/units/voct.go
new file mode 100644
index 0000000..c6a1a6b
--- /dev/null
+++ b/units/voct.go
@@ -0,0 +1,21 @@
+package units
+
+import "github.com/awonak/EuroPiGo/clamp"
+
+const (
+ MinVOct VOct = 0.0
+ MaxVOct VOct = 10.0
+)
+
+// VOct is a representation of a Volt-per-Octave value
+type VOct float32
+
+// ToVolts converts a V/Octave value to a value between 0.0 and 10.0 volts
+func (v VOct) ToVolts() float32 {
+ return v.ToFloat32()
+}
+
+// ToFloat32 returns a V/Octave value to its floating point representation [0.0 .. 10.0]
+func (v VOct) ToFloat32() float32 {
+ return float32(clamp.Clamp(v, MinVOct, MaxVOct))
+}
diff --git a/units/voct_test.go b/units/voct_test.go
new file mode 100644
index 0000000..35f474d
--- /dev/null
+++ b/units/voct_test.go
@@ -0,0 +1,120 @@
+package units_test
+
+import (
+ "math"
+ "testing"
+
+ "github.com/awonak/EuroPiGo/units"
+)
+
+func TestVOctToVolts(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := units.VOct(0.0)
+ if expected, actual := float32(0.0), min.ToVolts(); actual != expected {
+ t.Fatalf("VOct[%v] ToVolts: expected[%f] actual[%f]", min, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := units.VOct(10.0)
+ if expected, actual := float32(10.0), max.ToVolts(); actual != expected {
+ t.Fatalf("VOct[%v] ToVolts: expected[%f] actual[%f]", max, expected, actual)
+ }
+ })
+ })
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := units.VOct(-2.0)
+ if expected, actual := float32(0.0), belowMin.ToVolts(); actual != expected {
+ t.Fatalf("VOct[%v] ToVolts: expected[%f] actual[%f]", belowMin, expected, actual)
+ }
+ })
+
+ t.Run("AboveMax", func(t *testing.T) {
+ aboveMax := units.VOct(12.0)
+ if expected, actual := float32(10.0), aboveMax.ToVolts(); actual != expected {
+ t.Fatalf("VOct[%v] ToVolts: expected[%f] actual[%f]", aboveMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("NaN", func(t *testing.T) {
+ nan := units.VOct(math.NaN())
+ if actual := nan.ToVolts(); !math.IsNaN(float64(actual)) {
+ t.Fatalf("VOct[%v] ToVolts: expected[%f] actual[%f]", nan, math.NaN(), actual)
+ }
+ })
+
+ t.Run("Inf", func(t *testing.T) {
+ t.Run("Neg", func(t *testing.T) {
+ negInf := units.VOct(math.Inf(-1))
+ if expected, actual := float32(0.0), negInf.ToVolts(); actual != expected {
+ t.Fatalf("VOct[%v] ToVolts: expected[%f] actual[%f]", negInf, expected, actual)
+ }
+ })
+
+ t.Run("Pos", func(t *testing.T) {
+ posInf := units.VOct(math.Inf(1))
+ if expected, actual := float32(10.0), posInf.ToVolts(); actual != expected {
+ t.Fatalf("VOct[%v] ToVolts: expected[%f] actual[%f]", posInf, expected, actual)
+ }
+ })
+ })
+}
+
+func TestVOctToFloat32(t *testing.T) {
+ t.Run("InRange", func(t *testing.T) {
+ t.Run("Min", func(t *testing.T) {
+ min := units.VOct(0.0)
+ if expected, actual := float32(0.0), min.ToFloat32(); actual != expected {
+ t.Fatalf("VOct[%v] ToFloat32: expected[%f] actual[%f]", min, expected, actual)
+ }
+ })
+
+ t.Run("Max", func(t *testing.T) {
+ max := units.VOct(10.0)
+ if expected, actual := float32(10.0), max.ToFloat32(); actual != expected {
+ t.Fatalf("VOct[%v] ToFloat32: expected[%f] actual[%f]", max, expected, actual)
+ }
+ })
+ })
+ t.Run("OutOfRange", func(t *testing.T) {
+ t.Run("BelowMin", func(t *testing.T) {
+ belowMin := units.VOct(-2.0)
+ if expected, actual := float32(0.0), belowMin.ToFloat32(); actual != expected {
+ t.Fatalf("VOct[%v] ToFloat32: expected[%f] actual[%f]", belowMin, expected, actual)
+ }
+ })
+
+ t.Run("BelowMax", func(t *testing.T) {
+ aboveMax := units.VOct(122.0)
+ if expected, actual := float32(10.0), aboveMax.ToFloat32(); actual != expected {
+ t.Fatalf("VOct[%v] ToFloat32: expected[%f] actual[%f]", aboveMax, expected, actual)
+ }
+ })
+ })
+
+ t.Run("NaN", func(t *testing.T) {
+ nan := units.VOct(math.NaN())
+ if actual := nan.ToFloat32(); !math.IsNaN(float64(actual)) {
+ t.Fatalf("VOct[%v] ToFloat32: expected[%f] actual[%f]", nan, math.NaN(), actual)
+ }
+ })
+
+ t.Run("Inf", func(t *testing.T) {
+ t.Run("Neg", func(t *testing.T) {
+ negInf := units.VOct(math.Inf(-1))
+ if expected, actual := float32(0.0), negInf.ToFloat32(); actual != expected {
+ t.Fatalf("VOct[%v] ToFloat32: expected[%f] actual[%f]", negInf, expected, actual)
+ }
+ })
+
+ t.Run("Pos", func(t *testing.T) {
+ posInf := units.VOct(math.Inf(1))
+ if expected, actual := float32(10.0), posInf.ToFloat32(); actual != expected {
+ t.Fatalf("VOct[%v] ToFloat32: expected[%f] actual[%f]", posInf, expected, actual)
+ }
+ })
+ })
+}