From edbc7b530705c3548feff30eefefee13eba2b5f6 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 13 Mar 2023 14:52:24 -0700 Subject: [PATCH 01/62] Initial pass on cleanup --- analog_reader.go | 110 ----------------- helpers.go => debug.go | 11 -- digital_reader.go | 116 ------------------ europi.go | 68 +++++----- go.mod | 2 +- input/analog.go | 61 +++++++++ input/analogreader.go | 17 +++ input/button.go | 56 +++++++++ input/digital.go | 58 +++++++++ input/digitalreader.go | 14 +++ input/knob.go | 47 +++++++ internal/math/math.go | 16 +++ .../projects}/clockwerk/clockwerk.go | 14 ++- .../projects}/diagnostics/diagnostics.go | 20 +-- display.go => output/display.go | 6 +- outputer.go => output/output.go | 50 ++++---- output/pwm.go | 16 +++ 17 files changed, 364 insertions(+), 318 deletions(-) delete mode 100644 analog_reader.go rename helpers.go => debug.go (74%) delete mode 100644 digital_reader.go create mode 100644 input/analog.go create mode 100644 input/analogreader.go create mode 100644 input/button.go create mode 100644 input/digital.go create mode 100644 input/digitalreader.go create mode 100644 input/knob.go create mode 100644 internal/math/math.go rename {scripts => internal/projects}/clockwerk/clockwerk.go (93%) rename {scripts => internal/projects}/diagnostics/diagnostics.go (76%) rename display.go => output/display.go (90%) rename outputer.go => output/output.go (59%) create mode 100644 output/pwm.go 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/helpers.go b/debug.go similarity index 74% rename from helpers.go rename to debug.go index 832f92a..6784acd 100644 --- a/helpers.go +++ b/debug.go @@ -6,17 +6,6 @@ import ( "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) 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/europi.go b/europi.go index db5a15e..fc3057c 100644 --- a/europi.go +++ b/europi.go @@ -1,57 +1,55 @@ -package europi // import europi "github.com/awonak/EuroPiGo" +package europi // import "github.com/heucuva/europi" import ( "machine" -) -const ( - MaxVoltage = 10.0 - MinVoltage = 0.0 + "github.com/heucuva/europi/input" + "github.com/heucuva/europi/output" ) // 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 + Display *output.Display - DI DigitalReader - AI AnalogReader + DI input.DigitalReader + AI input.AnalogReader - B1 DigitalReader - B2 DigitalReader + B1 input.DigitalReader + B2 input.DigitalReader - K1 AnalogReader - K2 AnalogReader + K1 input.AnalogReader + K2 input.AnalogReader - CV1 Outputer - CV2 Outputer - CV3 Outputer - CV4 Outputer - CV5 Outputer - CV6 Outputer - CV [6]Outputer + CV1 output.Output + CV2 output.Output + CV3 output.Output + CV4 output.Output + CV5 output.Output + CV6 output.Output + CV [6]output.Output } // 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) + cv1 := output.NewOutput(machine.GPIO21, machine.PWM2) + cv2 := output.NewOutput(machine.GPIO20, machine.PWM2) + cv3 := output.NewOutput(machine.GPIO16, machine.PWM0) + cv4 := output.NewOutput(machine.GPIO17, machine.PWM0) + cv5 := output.NewOutput(machine.GPIO18, machine.PWM1) + cv6 := output.NewOutput(machine.GPIO19, machine.PWM1) - return &EuroPi{ - Display: NewDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1), + e := &EuroPi{ + Display: output.NewDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1), - DI: NewDI(machine.GPIO22), - AI: NewAI(machine.ADC0), + DI: input.NewDigital(machine.GPIO22), + AI: input.NewAnalog(machine.ADC0), - B1: NewButton(machine.GPIO4), - B2: NewButton(machine.GPIO5), + B1: input.NewButton(machine.GPIO4), + B2: input.NewButton(machine.GPIO5), - K1: NewKnob(machine.ADC1), - K2: NewKnob(machine.ADC2), + K1: input.NewKnob(machine.ADC1), + K2: input.NewKnob(machine.ADC2), CV1: cv1, CV2: cv2, @@ -59,6 +57,8 @@ func New() *EuroPi { CV4: cv4, CV5: cv5, CV6: cv5, - CV: [6]Outputer{cv1, cv2, cv3, cv4, cv5, cv6}, + CV: [6]output.Output{cv1, cv2, cv3, cv4, cv5, cv6}, } + + return e } diff --git a/go.mod b/go.mod index e221ece..5ceeebe 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/awonak/EuroPiGo +module github.com/heucuva/europi go 1.19 diff --git a/input/analog.go b/input/analog.go new file mode 100644 index 0000000..1c5b533 --- /dev/null +++ b/input/analog.go @@ -0,0 +1,61 @@ +package input + +import ( + "machine" + + europiMath "github.com/heucuva/europi/internal/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 + + MaxVoltage = 10.0 + MinVoltage = 0.0 +) + +// 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 Analog struct { + machine.ADC + samples uint16 +} + +// NewAnalog creates a new Analog. +func NewAnalog(pin machine.Pin) *Analog { + adc := machine.ADC{Pin: pin} + adc.Configure(machine.ADCConfig{}) + return &Analog{ADC: adc, samples: DefaultSamples} +} + +// Samples sets the number of reads for an more accurate average read. +func (a *Analog) 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 *Analog) Percent() float32 { + return float32(a.read()) / CalibratedMaxAI +} + +// ReadVoltage return the current read voltage between 0.0 and 10.0 volts. +func (a *Analog) 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 *Analog) Range(steps uint16) uint16 { + return uint16(a.Percent() * float32(steps)) +} + +func (a *Analog) read() uint16 { + var sum int + for i := 0; i < int(a.samples); i++ { + sum += europiMath.Clamp(int(a.Get())-CalibratedMinAI, 0, CalibratedMaxAI) + } + return uint16(sum / int(a.samples)) +} diff --git a/input/analogreader.go b/input/analogreader.go new file mode 100644 index 0000000..4e96ff8 --- /dev/null +++ b/input/analogreader.go @@ -0,0 +1,17 @@ +package input + +import ( + "machine" +) + +// 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 +} + +func init() { + machine.InitADC() +} diff --git a/input/button.go b/input/button.go new file mode 100644 index 0000000..9ec1c55 --- /dev/null +++ b/input/button.go @@ -0,0 +1,56 @@ +package input + +import ( + "machine" + "time" +) + +// 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/input/digital.go b/input/digital.go new file mode 100644 index 0000000..c23e77b --- /dev/null +++ b/input/digital.go @@ -0,0 +1,58 @@ +package input + +import ( + "machine" + "time" +) + +const DefaultDebounceDelay = time.Duration(50 * time.Millisecond) + +// Digital is a struct for handling reading of the digital input. +type Digital struct { + Pin machine.Pin + debounceDelay time.Duration + lastInput time.Time + callback func(p machine.Pin) +} + +// NewDigital creates a new Digital struct. +func NewDigital(pin machine.Pin) *Digital { + pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) + return &Digital{ + Pin: pin, + lastInput: time.Now(), + debounceDelay: DefaultDebounceDelay, + } +} + +// LastInput return the time of the last high input (triggered at 0.8v). +func (d *Digital) LastInput() time.Time { + return d.lastInput +} + +// Value returns true if the input is high (above 0.8v), else false. +func (d *Digital) 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 *Digital) 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 *Digital) HandlerWithDebounce(handler func(p machine.Pin), delay time.Duration) { + d.callback = handler + d.debounceDelay = delay + d.Pin.SetInterrupt(machine.PinFalling, d.debounceWrapper) +} + +func (d *Digital) debounceWrapper(p machine.Pin) { + t := time.Now() + if t.Before(d.lastInput.Add(d.debounceDelay)) { + return + } + d.callback(p) + d.lastInput = t +} diff --git a/input/digitalreader.go b/input/digitalreader.go new file mode 100644 index 0000000..9c1d50a --- /dev/null +++ b/input/digitalreader.go @@ -0,0 +1,14 @@ +package input + +import ( + "machine" + "time" +) + +// 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 +} diff --git a/input/knob.go b/input/knob.go new file mode 100644 index 0000000..f15fba8 --- /dev/null +++ b/input/knob.go @@ -0,0 +1,47 @@ +package input + +import ( + "machine" + "math" +) + +// 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/internal/math/math.go b/internal/math/math.go new file mode 100644 index 0000000..6bf94ba --- /dev/null +++ b/internal/math/math.go @@ -0,0 +1,16 @@ +package math + +type Clampable interface { + ~uint8 | ~uint16 | ~int | ~float32 +} + +// Clamp returns a value that is no lower than "low" and no higher than "high". +func Clamp[V Clampable](value, low, high V) V { + if value >= high { + return high + } + if value <= low { + return low + } + return value +} diff --git a/scripts/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go similarity index 93% rename from scripts/clockwerk/clockwerk.go rename to internal/projects/clockwerk/clockwerk.go index 217158c..5e978c6 100644 --- a/scripts/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -34,7 +34,9 @@ import ( "tinygo.org/x/tinydraw" - europi "github.com/awonak/EuroPiGo" + "github.com/heucuva/europi" + europiMath "github.com/heucuva/europi/internal/math" + "github.com/heucuva/europi/output" ) const ( @@ -203,12 +205,12 @@ 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.Display.WriteLine(text, int16(i*output.OLEDWidth/len(c.clocks))+2, 26) } - xWidth := int16(europi.OLEDWidth / len(c.clocks)) + xWidth := int16(output.OLEDWidth / len(c.clocks)) xOffset := int16(c.selected) * xWidth // TODO: replace box with chevron. - tinydraw.Rectangle(c.Display, xOffset, 16, xWidth, 16, europi.White) + tinydraw.Rectangle(c.Display, xOffset, 16, xWidth, 16, output.White) c.Display.Display() } @@ -235,7 +237,7 @@ func main() { c.doClockReset = true return } - c.selected = uint8(europi.Clamp(int(c.selected)-1, 0, len(c.clocks))) + c.selected = uint8(europiMath.Clamp(int(c.selected)-1, 0, len(c.clocks))) c.displayShouldUpdate = true }) @@ -245,7 +247,7 @@ func main() { c.doClockReset = true return } - c.selected = uint8(europi.Clamp(int(c.selected)+1, 0, len(c.clocks)-1)) + c.selected = uint8(europiMath.Clamp(int(c.selected)+1, 0, len(c.clocks)-1)) c.displayShouldUpdate = true }) diff --git a/scripts/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go similarity index 76% rename from scripts/diagnostics/diagnostics.go rename to internal/projects/diagnostics/diagnostics.go index f6c1043..c885430 100644 --- a/scripts/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -1,4 +1,4 @@ -// Diagnostics is a script for demonstrating all main interactions with the EuroPiGo firmware. +// Diagnostics is a script for demonstrating all main interactions with the europi-go firmware. package main import ( @@ -7,7 +7,9 @@ import ( "tinygo.org/x/tinydraw" - europi "github.com/awonak/EuroPiGo" + "github.com/heucuva/europi" + "github.com/heucuva/europi/input" + "github.com/heucuva/europi/output" ) type MyApp struct { @@ -32,14 +34,14 @@ func main() { }) e.B2.Handler(func(p machine.Pin) { - myApp.staticCv = (myApp.staticCv + 1) % europi.MaxVoltage + myApp.staticCv = (myApp.staticCv + 1) % input.MaxVoltage }) for { e.Display.ClearBuffer() // Highlight the border of the oled display. - tinydraw.Rectangle(e.Display, 0, 0, 128, 32, europi.White) + tinydraw.Rectangle(e.Display, 0, 0, 128, 32, output.White) // Display analog and digital input values. inputText := fmt.Sprintf("din: %5v ain: %2.2f ", e.DI.Value(), e.AI.Percent()) @@ -61,18 +63,18 @@ func main() { // 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()) + e.CV1.SetVoltage(e.K1.ReadVoltage()) + e.CV4.SetVoltage(output.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()) + e.CV2.SetVoltage(e.K2.ReadVoltage()) + e.CV5.SetVoltage(output.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)) + e.CV6.SetVoltage(float32(myApp.staticCv)) myApp.prevStaticCv = myApp.staticCv } } diff --git a/display.go b/output/display.go similarity index 90% rename from display.go rename to output/display.go index 15fbad8..0eaf418 100644 --- a/display.go +++ b/output/display.go @@ -1,4 +1,4 @@ -package europi +package output import ( "image/color" @@ -45,8 +45,8 @@ func NewDisplay(channel *machine.I2C, sdaPin, sclPin machine.Pin) *Display { return &Display{Device: display, font: DefaultFont} } -// Font overrides the default font used by `WriteLine`. -func (d *Display) Font(font *tinyfont.Font) { +// SetFont overrides the default font used by `WriteLine`. +func (d *Display) SetFont(font *tinyfont.Font) { d.font = font } diff --git a/outputer.go b/output/output.go similarity index 59% rename from outputer.go rename to output/output.go index 4512bad..566b86b 100644 --- a/outputer.go +++ b/output/output.go @@ -1,8 +1,10 @@ -package europi +package output import ( "log" "machine" + + europiMath "github.com/heucuva/europi/internal/math" ) const ( @@ -11,40 +13,32 @@ const ( 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 + + MaxVoltage = 10.0 + MinVoltage = 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. 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) +// Output is an interface for interacting with the cv output jacks. +type Output interface { + Get() uint32 + SetVoltage(v float32) On() Off() } -// Outputer is struct for interacting with the cv output jacks. -type Output struct { - pwm PWMer +// Output is struct for interacting with the cv output jacks. +type output struct { + pwm PWM pin machine.Pin ch uint8 } -// NewOutput returns a new Output struct. -func NewOutput(pin machine.Pin, pwm PWMer) *Output { +// NewOutput returns a new Output interface. +func NewOutput(pin machine.Pin, pwm PWM) Output { err := pwm.Configure(machine.PWMConfig{ Period: defaultPeriod, }) @@ -59,17 +53,17 @@ func NewOutput(pin machine.Pin, pwm PWMer) *Output { log.Fatal("pwm Channel error: ", err.Error()) } - return &Output{pwm, pin, ch} + return &output{pwm, pin, ch} } // Get returns the current set voltage in the range of 0 to pwm.Top(). -func (o *Output) Get() uint32 { +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) +// SetVoltage sets the current output voltage within a range of 0.0 to 10.0. +func (o *output) SetVoltage(v float32) { + v = europiMath.Clamp(v, MinVoltage, MaxVoltage) invertedCv := (v / MaxVoltage) * float32(o.pwm.Top()) // cv := (float32(o.pwm.Top()) - invertedCv) - CalibratedOffset cv := float32(invertedCv) - CalibratedOffset @@ -77,11 +71,11 @@ func (o *Output) Voltage(v float32) { } // On sets the current voltage high at 10.0v. -func (o *Output) On() { +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() { +func (o *output) Off() { o.pwm.Set(o.ch, 0) } diff --git a/output/pwm.go b/output/pwm.go new file mode 100644 index 0000000..7aa8759 --- /dev/null +++ b/output/pwm.go @@ -0,0 +1,16 @@ +package output + +import ( + "machine" +) + +// PWM is an interface for interacting with a machine.pwmGroup +type PWM 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 +} From 45d92a0a6e88b684dfa71df4eb0c2ace561b78a6 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 16 Mar 2023 23:24:33 -0700 Subject: [PATCH 02/62] Add some functions from current europi firmware --- config/file.go | 47 ++++++++++++ experimental/knobbank.go | 126 ++++++++++++++++++++++++++++++++ experimental/knobbankoptions.go | 37 ++++++++++ experimental/knoboptions.go | 15 ++++ input/analog.go | 5 ++ input/analogreader.go | 1 + input/knob.go | 6 ++ internal/math/lerp.go | 9 +++ output/display.go | 6 ++ output/output.go | 10 +++ 10 files changed, 262 insertions(+) create mode 100644 config/file.go create mode 100644 experimental/knobbank.go create mode 100644 experimental/knobbankoptions.go create mode 100644 experimental/knoboptions.go create mode 100644 internal/math/lerp.go diff --git a/config/file.go b/config/file.go new file mode 100644 index 0000000..c05324c --- /dev/null +++ b/config/file.go @@ -0,0 +1,47 @@ +package config + +import ( + "bytes" + "encoding/json" + "os" + "sync" +) + +type File[TConfig any] struct { + filename string + Config TConfig + mu sync.Mutex +} + +func NewFile[TConfig any](filename string, defaults TConfig) *File[TConfig] { + cf := &File[TConfig]{ + filename: filename, + Config: defaults, + } + + _ = cf.Read() + + return cf +} + +func (c *File[TConfig]) Read() error { + cfb, err := os.ReadFile(c.filename) + if err != nil { + return err + } + return json.NewDecoder(bytes.NewReader(cfb)).Decode(&c.Config) +} + +func (c *File[TConfig]) Write() error { + c.mu.Lock() + defer c.mu.Unlock() + cfb := &bytes.Buffer{} + if err := json.NewEncoder(cfb).Encode(&c.Config); err != nil { + return err + } + return os.WriteFile(c.filename, cfb.Bytes(), os.ModePerm) +} + +func (c *File[TConfig]) Flush() error { + return c.Write() +} diff --git a/experimental/knobbank.go b/experimental/knobbank.go new file mode 100644 index 0000000..7a6334d --- /dev/null +++ b/experimental/knobbank.go @@ -0,0 +1,126 @@ +package experimental + +import ( + "github.com/heucuva/europi/input" + "github.com/heucuva/europi/internal/math" +) + +type KnobBank struct { + knob input.AnalogReader + current int + bank []knobBankEntry +} + +func NewKnobBank(knob input.AnalogReader, opts ...KnobBankOption) (*KnobBank, error) { + kb := &KnobBank{ + knob: knob, + } + + for _, opt := range opts { + if err := opt(kb); err != nil { + return nil, err + } + } + + return kb, nil +} + +func (kb *KnobBank) CurrentName() string { + if len(kb.bank) == 0 { + return "" + } + return kb.bank[kb.current].name +} + +func (kb *KnobBank) Current() input.AnalogReader { + return kb +} + +func (kb *KnobBank) Samples(samples uint16) { + kb.knob.Samples(samples) +} + +func (kb *KnobBank) ReadVoltage() float32 { + if len(kb.bank) == 0 { + return kb.knob.ReadVoltage() + } + + cur := &kb.bank[kb.current] + cur.update(kb.knob) + return cur.value +} + +func (kb *KnobBank) Percent() float32 { + if len(kb.bank) == 0 { + return kb.knob.Percent() + } + + cur := &kb.bank[kb.current] + cur.update(kb.knob) + return cur.percent +} + +func (kb *KnobBank) Range(steps uint16) uint16 { + return kb.knob.Range(steps) +} + +func (kb *KnobBank) Choice(numItems int) int { + if len(kb.bank) == 0 { + return int(kb.Range(uint16(numItems))) + } + + cur := &kb.bank[kb.current] + cur.update(kb.knob) + return math.Lerp(cur.percent, 0, numItems-1) +} + +func (kb *KnobBank) Next() { + if len(kb.bank) == 0 { + kb.current = 0 + return + } + + kb.bank[kb.current].lock(kb.knob) + kb.current++ + if kb.current >= len(kb.bank) { + kb.current = 0 + } + kb.bank[kb.current].unlock() +} + +type knobBankEntry struct { + name string + enabled bool + locked bool + value float32 + percent float32 + minVoltage float32 + maxVoltage float32 + scale float32 +} + +func (e *knobBankEntry) lock(knob input.AnalogReader) { + if e.locked { + return + } + + e.update(knob) + e.locked = true +} + +func (e *knobBankEntry) unlock() { + if !e.enabled { + return + } + + e.locked = false +} + +func (e *knobBankEntry) update(knob input.AnalogReader) { + if !e.enabled || e.locked { + return + } + + e.percent = math.Lerp[float32](knob.Percent()*e.scale, 0, 1) + e.value = math.Clamp(knob.ReadVoltage()*e.scale, e.minVoltage, e.maxVoltage) +} diff --git a/experimental/knobbankoptions.go b/experimental/knobbankoptions.go new file mode 100644 index 0000000..97f4e86 --- /dev/null +++ b/experimental/knobbankoptions.go @@ -0,0 +1,37 @@ +package experimental + +import ( + "fmt" + + "github.com/heucuva/europi/input" +) + +type KnobBankOption func(kb *KnobBank) error + +func WithDisabledKnob() KnobBankOption { + return func(kb *KnobBank) error { + kb.bank = append(kb.bank, knobBankEntry{}) + return nil + } +} + +func WithLockedKnob(name string, opts ...KnobOption) KnobBankOption { + return func(kb *KnobBank) error { + e := knobBankEntry{ + name: name, + enabled: true, + minVoltage: input.MinVoltage, + maxVoltage: input.MaxVoltage, + 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/knoboptions.go b/experimental/knoboptions.go new file mode 100644 index 0000000..cad1308 --- /dev/null +++ b/experimental/knoboptions.go @@ -0,0 +1,15 @@ +package experimental + +import "fmt" + +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 + return nil + } +} diff --git a/input/analog.go b/input/analog.go index 1c5b533..b0d57a2 100644 --- a/input/analog.go +++ b/input/analog.go @@ -4,6 +4,7 @@ import ( "machine" europiMath "github.com/heucuva/europi/internal/math" + europim "github.com/heucuva/europi/internal/math" ) const ( @@ -52,6 +53,10 @@ func (a *Analog) Range(steps uint16) uint16 { return uint16(a.Percent() * float32(steps)) } +func (a *Analog) Choice(numItems int) int { + return europim.Lerp(a.Percent(), 0, numItems-1) +} + func (a *Analog) read() uint16 { var sum int for i := 0; i < int(a.samples); i++ { diff --git a/input/analogreader.go b/input/analogreader.go index 4e96ff8..9c540de 100644 --- a/input/analogreader.go +++ b/input/analogreader.go @@ -10,6 +10,7 @@ type AnalogReader interface { ReadVoltage() float32 Percent() float32 Range(steps uint16) uint16 + Choice(numItems int) int } func init() { diff --git a/input/knob.go b/input/knob.go index f15fba8..29331c4 100644 --- a/input/knob.go +++ b/input/knob.go @@ -3,6 +3,8 @@ package input import ( "machine" "math" + + europim "github.com/heucuva/europi/internal/math" ) // A struct for handling the reading of knob voltage and position. @@ -38,6 +40,10 @@ func (k *Knob) Range(steps uint16) uint16 { return uint16(k.Percent() * float32(steps)) } +func (k *Knob) Choice(numItems int) int { + return europim.Lerp(k.Percent(), 0, numItems-1) +} + func (k *Knob) read() uint16 { var sum int for i := 0; i < int(k.samples); i++ { diff --git a/internal/math/lerp.go b/internal/math/lerp.go new file mode 100644 index 0000000..086a454 --- /dev/null +++ b/internal/math/lerp.go @@ -0,0 +1,9 @@ +package math + +type Lerpable interface { + ~uint8 | ~uint16 | ~int | ~float32 +} + +func Lerp[V Lerpable](t float32, low, high V) V { + return V(t*float32(high-low)) + low +} diff --git a/output/display.go b/output/display.go index 0eaf418..faebc37 100644 --- a/output/display.go +++ b/output/display.go @@ -5,6 +5,7 @@ import ( "machine" "tinygo.org/x/drivers/ssd1306" + "tinygo.org/x/tinydraw" "tinygo.org/x/tinyfont" "tinygo.org/x/tinyfont/proggy" ) @@ -54,3 +55,8 @@ func (d *Display) SetFont(font *tinyfont.Font) { func (d *Display) WriteLine(text string, x, y int16) { tinyfont.WriteLine(d, d.font, x, y, text, White) } + +// DrawHLine draws a horizontal line +func (d *Display) DrawHLine(x, y, xLen int16, c color.RGBA) { + tinydraw.Line(d, x, y, x+xLen-1, y, c) +} diff --git a/output/output.go b/output/output.go index 566b86b..1c7163d 100644 --- a/output/output.go +++ b/output/output.go @@ -26,6 +26,7 @@ var defaultPeriod uint64 = 500 type Output interface { Get() uint32 SetVoltage(v float32) + Set(v bool) On() Off() } @@ -61,6 +62,15 @@ func (o *output) Get() uint32 { return o.pwm.Get(o.ch) } +// Set updates the current voltage high (true) or low (false) +func (o *output) Set(v bool) { + if v { + o.On() + } else { + o.Off() + } +} + // SetVoltage sets the current output voltage within a range of 0.0 to 10.0. func (o *output) SetVoltage(v float32) { v = europiMath.Clamp(v, MinVoltage, MaxVoltage) From ff50fa47c616c048f04042ce7276cc5f1ea3b044 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 17 Mar 2023 09:20:47 -0700 Subject: [PATCH 03/62] Swap out deprecated and magic numbers --- output/display.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/output/display.go b/output/display.go index faebc37..d3e308d 100644 --- a/output/display.go +++ b/output/display.go @@ -11,8 +11,8 @@ import ( ) const ( - OLEDFreq = machine.TWI_FREQ_400KHZ - OLEDAddr = 0x3C + OLEDFreq = machine.KHz * 400 + OLEDAddr = ssd1306.Address_128_32 OLEDWidth = 128 OLEDHeight = 32 ) From 8d93e62ec33d96f3cd25829174194c73a7410e65 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 18 Mar 2023 14:40:32 -0700 Subject: [PATCH 04/62] Basic bootstrap support --- bootstrap.go | 161 ++++++++++++++++++++++++++++++++++ bootstrap_lifecycle.go | 25 ++++++ bootstrapoptions.go | 24 +++++ bootstrapoptions_features.go | 17 ++++ bootstrapoptions_lifecycle.go | 132 ++++++++++++++++++++++++++++ bootstrapoptions_settings.go | 9 ++ 6 files changed, 368 insertions(+) create mode 100644 bootstrap.go create mode 100644 bootstrap_lifecycle.go create mode 100644 bootstrapoptions.go create mode 100644 bootstrapoptions_features.go create mode 100644 bootstrapoptions_lifecycle.go create mode 100644 bootstrapoptions_settings.go diff --git a/bootstrap.go b/bootstrap.go new file mode 100644 index 0000000..1f5524f --- /dev/null +++ b/bootstrap.go @@ -0,0 +1,161 @@ +package europi + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/heucuva/europi/output" +) + +var ( + // Pi is a global EuroPi instance constructed by calling the Bootstrap() function + Pi *EuroPi + + piWantDestroyChan chan struct{} +) + +// Bootstrap will set up a global runtime environment (see europi.Pi) +func Bootstrap(options ...BootstrapOption) error { + config := bootstrapConfig{ + mainLoopInterval: DefaultMainLoopInterval, + vfsEnable: DefaultEnableVirtualFileSystem, + + onPostBootstrapConstructionFn: DefaultPostBootstrapInitialization, + onPreInitializeComponentsFn: nil, + onPostInitializeComponentsFn: nil, + onBootstrapCompletedFn: DefaultBootstrapCompleted, + onStartLoopFn: nil, + onMainLoopFn: DefaultMainLoop, + onEndLoopFn: nil, + onBeginDestroyFn: nil, + onFinishDestroyFn: nil, + } + + for _, opt := range options { + if err := opt(&config); err != nil { + return err + } + } + + e := New() + + Pi = e + piWantDestroyChan = make(chan struct{}, 1) + + defer func() { + if err := recover(); err != nil { + fnt := output.DefaultFont + e.Display.SetFont(fnt) + e.Display.WriteLine(fmt.Sprint(err), 0, int16(fnt.YAdvance)) + _ = e.Display.Display() + } + }() + + var onceBootstrapDestroy sync.Once + runBootstrapDestroy := func() { + onceBootstrapDestroy.Do(func() { + bootstrapDestroy(&config, e) + }) + } + defer runBootstrapDestroy() + + if config.onPostBootstrapConstructionFn != nil { + config.onPostBootstrapConstructionFn(e) + } + + bootstrapInitializeComponents(&config, e) + + if config.onBootstrapCompletedFn != nil { + config.onBootstrapCompletedFn(e) + } + + bootstrapRunLoop(&config, e) + + return nil +} + +func Shutdown() error { + if piWantDestroyChan == nil { + return errors.New("cannot shutdown: no available bootstrap") + } + + piWantDestroyChan <- struct{}{} + return nil +} + +func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) { + if config.onPreInitializeComponentsFn != nil { + config.onPreInitializeComponentsFn(e) + } + + // TODO: initialize components + + if config.onPostInitializeComponentsFn != nil { + config.onPostInitializeComponentsFn(e) + } +} + +func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { + if config.onStartLoopFn != nil { + config.onStartLoopFn(e) + } + + if config.mainLoopInterval > 0 { + bootstrapRunLoopWithDelay(config, e) + } else { + bootstrapRunLoopNoDelay(config, e) + } + + if config.onEndLoopFn != nil { + config.onEndLoopFn(e) + } +} + +func bootstrapRunLoopWithDelay(config *bootstrapConfig, e *EuroPi) { + ticker := time.NewTicker(config.mainLoopInterval) + defer ticker.Stop() + + lastTick := time.Now() +mainLoop: + for { + select { + case <-piWantDestroyChan: + break mainLoop + + case now := <-ticker.C: + config.onMainLoopFn(e, now.Sub(lastTick)) + lastTick = now + } + } +} + +func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { + lastTick := time.Now() +mainLoop: + for { + select { + case <-piWantDestroyChan: + break mainLoop + + default: + now := time.Now() + config.onMainLoopFn(e, now.Sub(lastTick)) + lastTick = now + } + } +} + +func bootstrapDestroy(config *bootstrapConfig, e *EuroPi) { + if config.onBeginDestroyFn != nil { + config.onBeginDestroyFn(e) + } + + close(piWantDestroyChan) + Pi = nil + + if config.onFinishDestroyFn != nil { + config.onFinishDestroyFn(e) + } +} diff --git a/bootstrap_lifecycle.go b/bootstrap_lifecycle.go new file mode 100644 index 0000000..34d6269 --- /dev/null +++ b/bootstrap_lifecycle.go @@ -0,0 +1,25 @@ +package europi + +import "time" + +func DefaultPostBootstrapInitialization(e *EuroPi) { + e.Display.ClearBuffer() + if err := e.Display.Display(); err != nil { + panic(err) + } +} + +func DefaultBootstrapCompleted(e *EuroPi) { + e.Display.ClearBuffer() + if err := e.Display.Display(); err != nil { + panic(err) + } +} + +const ( + DefaultMainLoopInterval time.Duration = time.Millisecond * 100 +) + +// DefaultMainLoop is the default main loop used if a new one is not specified to Bootstrap() +func DefaultMainLoop(e *EuroPi, deltaTime time.Duration) { +} diff --git a/bootstrapoptions.go b/bootstrapoptions.go new file mode 100644 index 0000000..6b06e2e --- /dev/null +++ b/bootstrapoptions.go @@ -0,0 +1,24 @@ +package europi + +import ( + "time" +) + +// BootstrapOption is a single configuration parameter passed to the Bootstrap() function +type BootstrapOption func(o *bootstrapConfig) error + +type bootstrapConfig struct { + mainLoopInterval time.Duration + vfsEnable bool + + // lifecycle callbacks + onPostBootstrapConstructionFn PostBootstrapConstructionFunc + onPreInitializeComponentsFn PreInitializeComponentsFunc + onPostInitializeComponentsFn PostInitializeComponentsFunc + onBootstrapCompletedFn BootstrapCompletedFunc + onStartLoopFn StartLoopFunc + onMainLoopFn MainLoopFunc + onEndLoopFn EndLoopFunc + onBeginDestroyFn BeginDestroyFunc + onFinishDestroyFn FinishDestroyFunc +} diff --git a/bootstrapoptions_features.go b/bootstrapoptions_features.go new file mode 100644 index 0000000..30ee5dd --- /dev/null +++ b/bootstrapoptions_features.go @@ -0,0 +1,17 @@ +package europi + +import ( + "errors" + "time" +) + +// MainLoopInterval sets the interval between calls to the configured main loop function +func MainLoopInterval(interval time.Duration) BootstrapOption { + return func(o *bootstrapConfig) error { + if interval < 0 { + return errors.New("interval must be greater than or equal to 0") + } + o.mainLoopInterval = interval + return nil + } +} diff --git a/bootstrapoptions_lifecycle.go b/bootstrapoptions_lifecycle.go new file mode 100644 index 0000000..2e1ff2f --- /dev/null +++ b/bootstrapoptions_lifecycle.go @@ -0,0 +1,132 @@ +package europi + +import ( + "errors" + "time" +) + +/* Order of lifecycle calls: +BootStrap + | + V +Callback: PostBootstrapConstruction + | + V +Bootstrap: postBootstrapConstruction + | + V + Callback: PreInitializeComponents + | + V + Bootstrap: initializeComponents + | + V + Callback: PostInitializeComponents + | + V +Callback: BootstrapCompleted + | + V +Bootstrap: runLoop + | + V + Callback: StartLoop + | + V + Callback(on tick): MainLoop + | + V + Callback: EndLoop + | + V +Bootstrap: destroyBootstrap + | + V + Callback: BeginDestroy + | + V + Callback: FinishDestroy +*/ + +type ( + PostBootstrapConstructionFunc func(e *EuroPi) + PreInitializeComponentsFunc func(e *EuroPi) + PostInitializeComponentsFunc func(e *EuroPi) + BootstrapCompletedFunc func(e *EuroPi) + StartLoopFunc func(e *EuroPi) + MainLoopFunc func(e *EuroPi, deltaTime time.Duration) + EndLoopFunc func(e *EuroPi) + BeginDestroyFunc func(e *EuroPi) + FinishDestroyFunc func(e *EuroPi) +) + +// PostBootstrapConstruction runs immediately after primary EuroPi bootstrap has finished, +// but before components have been initialized +func PostBootstrapConstruction(fn PostBootstrapConstructionFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + o.onPostBootstrapConstructionFn = fn + return nil + } +} + +func PreInitializeComponents(fn PreInitializeComponentsFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + o.onPreInitializeComponentsFn = fn + return nil + } +} + +func PostInitializeComponents(fn PostInitializeComponentsFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + o.onPostInitializeComponentsFn = fn + return nil + } +} + +func BootstrapCompleted(fn BootstrapCompletedFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + o.onBootstrapCompletedFn = fn + return nil + } +} + +func StartLoop(fn StartLoopFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + o.onStartLoopFn = fn + return nil + } +} + +// MainLoop sets the main loop function to be called on interval. +// nil is not allowed - if you want to set the default, either do not specify a MainLoop() option +// or specify europi.DefaultMainLoop +func MainLoop(fn MainLoopFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + if fn == nil { + return errors.New("a valid main loop function must be specified") + } + o.onMainLoopFn = fn + return nil + } +} + +func EndLoop(fn EndLoopFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + o.onEndLoopFn = fn + return nil + } +} + +func BeginDestroy(fn BeginDestroyFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + o.onBeginDestroyFn = fn + return nil + } +} + +func FinishDestroy(fn FinishDestroyFunc) BootstrapOption { + return func(o *bootstrapConfig) error { + o.onFinishDestroyFn = fn + return nil + } +} diff --git a/bootstrapoptions_settings.go b/bootstrapoptions_settings.go new file mode 100644 index 0000000..228ae56 --- /dev/null +++ b/bootstrapoptions_settings.go @@ -0,0 +1,9 @@ +package europi + +// EnableVirtualFileSystem sets the enable flag for the virtual file system (backed by flash memory) +func EnableVirtualFileSystem(enable bool) BootstrapOption { + return func(o *bootstrapConfig) error { + o.vfsEnable = enable + return nil + } +} From 73600de6a0b8b64e0ff430de640c48f199e01d3b Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 21 Mar 2023 16:56:55 -0700 Subject: [PATCH 05/62] Remove vfs --- bootstrap.go | 1 - bootstrapoptions.go | 1 - bootstrapoptions_settings.go | 9 ------- config/file.go | 47 ------------------------------------ config/setting.go | 46 +++++++++++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 58 deletions(-) delete mode 100644 bootstrapoptions_settings.go delete mode 100644 config/file.go create mode 100644 config/setting.go diff --git a/bootstrap.go b/bootstrap.go index 1f5524f..18a1b76 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -20,7 +20,6 @@ var ( func Bootstrap(options ...BootstrapOption) error { config := bootstrapConfig{ mainLoopInterval: DefaultMainLoopInterval, - vfsEnable: DefaultEnableVirtualFileSystem, onPostBootstrapConstructionFn: DefaultPostBootstrapInitialization, onPreInitializeComponentsFn: nil, diff --git a/bootstrapoptions.go b/bootstrapoptions.go index 6b06e2e..59b1c7b 100644 --- a/bootstrapoptions.go +++ b/bootstrapoptions.go @@ -9,7 +9,6 @@ type BootstrapOption func(o *bootstrapConfig) error type bootstrapConfig struct { mainLoopInterval time.Duration - vfsEnable bool // lifecycle callbacks onPostBootstrapConstructionFn PostBootstrapConstructionFunc diff --git a/bootstrapoptions_settings.go b/bootstrapoptions_settings.go deleted file mode 100644 index 228ae56..0000000 --- a/bootstrapoptions_settings.go +++ /dev/null @@ -1,9 +0,0 @@ -package europi - -// EnableVirtualFileSystem sets the enable flag for the virtual file system (backed by flash memory) -func EnableVirtualFileSystem(enable bool) BootstrapOption { - return func(o *bootstrapConfig) error { - o.vfsEnable = enable - return nil - } -} diff --git a/config/file.go b/config/file.go deleted file mode 100644 index c05324c..0000000 --- a/config/file.go +++ /dev/null @@ -1,47 +0,0 @@ -package config - -import ( - "bytes" - "encoding/json" - "os" - "sync" -) - -type File[TConfig any] struct { - filename string - Config TConfig - mu sync.Mutex -} - -func NewFile[TConfig any](filename string, defaults TConfig) *File[TConfig] { - cf := &File[TConfig]{ - filename: filename, - Config: defaults, - } - - _ = cf.Read() - - return cf -} - -func (c *File[TConfig]) Read() error { - cfb, err := os.ReadFile(c.filename) - if err != nil { - return err - } - return json.NewDecoder(bytes.NewReader(cfb)).Decode(&c.Config) -} - -func (c *File[TConfig]) Write() error { - c.mu.Lock() - defer c.mu.Unlock() - cfb := &bytes.Buffer{} - if err := json.NewEncoder(cfb).Encode(&c.Config); err != nil { - return err - } - return os.WriteFile(c.filename, cfb.Bytes(), os.ModePerm) -} - -func (c *File[TConfig]) Flush() error { - return c.Write() -} diff --git a/config/setting.go b/config/setting.go new file mode 100644 index 0000000..54f2eed --- /dev/null +++ b/config/setting.go @@ -0,0 +1,46 @@ +package config + +import ( + "runtime/interrupt" + "runtime/volatile" + "unsafe" +) + +const ( + picoFlashStart = 0x10000000 +) + +type Setting[T any] struct { + Value T + initialized bool +} + +// Init initializes the setting with the expected default or loads the saved value from pico flash +func (s *Setting[T]) Init(value T) { + valuePtr := unsafe.Slice(&s.Value, 1) + vfp := unsafe.Add(unsafe.Pointer(&s.Value), picoFlashStart) + valueFlashPtr := unsafe.Slice((*T)(vfp), 1) + initializedFlashPtr := (*uint8)(unsafe.Add(unsafe.Pointer(&s.initialized), picoFlashStart)) + + state := interrupt.Disable() + if iv := volatile.LoadUint8(initializedFlashPtr); iv == 0 { + s.Value = value + } else { + copy(valuePtr, valueFlashPtr) + s.initialized = true + } + interrupt.Restore(state) +} + +// Flush flushes the value out to pico flash storage along with the initialize flag +func (s *Setting[T]) Flush() { + valuePtr := unsafe.Slice(&s.Value, 1) + vfp := unsafe.Add(unsafe.Pointer(&s.Value), picoFlashStart) + valueFlashPtr := unsafe.Slice((*T)(vfp), 1) + initializedFlashPtr := (*uint8)(unsafe.Add(unsafe.Pointer(&s.initialized), picoFlashStart)) + + state := interrupt.Disable() + copy(valueFlashPtr, valuePtr) + volatile.StoreUint8(initializedFlashPtr, 1) + interrupt.Restore(state) +} From 371527a7520bccddb843925cd10962d8c8df7a1b Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 21 Mar 2023 18:49:33 -0700 Subject: [PATCH 06/62] Fix up knob bank --- experimental/knobbank.go | 66 +++++++++++++++++++++++---------- experimental/knobbankoptions.go | 1 + experimental/knoboptions.go | 9 ++++- internal/math/math.go | 9 +++++ 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/experimental/knobbank.go b/experimental/knobbank.go index 7a6334d..87c6607 100644 --- a/experimental/knobbank.go +++ b/experimental/knobbank.go @@ -6,14 +6,16 @@ import ( ) type KnobBank struct { - knob input.AnalogReader - current int - bank []knobBankEntry + knob input.AnalogReader + current int + lastValue float32 + bank []knobBankEntry } func NewKnobBank(knob input.AnalogReader, opts ...KnobBankOption) (*KnobBank, error) { kb := &KnobBank{ - knob: knob, + knob: knob, + lastValue: knob.ReadVoltage(), } for _, opt := range opts { @@ -41,23 +43,27 @@ func (kb *KnobBank) Samples(samples uint16) { } func (kb *KnobBank) ReadVoltage() float32 { + value := kb.knob.ReadVoltage() if len(kb.bank) == 0 { - return kb.knob.ReadVoltage() + return value } cur := &kb.bank[kb.current] - cur.update(kb.knob) - return cur.value + percent := kb.knob.Percent() + kb.lastValue = cur.update(percent, value, kb.lastValue) + return cur.Value() } func (kb *KnobBank) Percent() float32 { + percent := kb.knob.Percent() if len(kb.bank) == 0 { - return kb.knob.Percent() + return percent } cur := &kb.bank[kb.current] - cur.update(kb.knob) - return cur.percent + value := kb.knob.ReadVoltage() + kb.lastValue = cur.update(percent, value, kb.lastValue) + return cur.Percent() } func (kb *KnobBank) Range(steps uint16) uint16 { @@ -70,8 +76,11 @@ func (kb *KnobBank) Choice(numItems int) int { } cur := &kb.bank[kb.current] - cur.update(kb.knob) - return math.Lerp(cur.percent, 0, numItems-1) + value := kb.knob.ReadVoltage() + percent := kb.knob.Percent() + kb.lastValue = cur.update(percent, value, kb.lastValue) + idx := math.Lerp(cur.Percent(), 0, 2*numItems+1) / 2 + return math.Clamp(idx, 0, numItems-1) } func (kb *KnobBank) Next() { @@ -80,7 +89,9 @@ func (kb *KnobBank) Next() { return } - kb.bank[kb.current].lock(kb.knob) + cur := &kb.bank[kb.current] + cur.lock(kb.knob, kb.lastValue) + kb.current++ if kb.current >= len(kb.bank) { kb.current = 0 @@ -99,13 +110,15 @@ type knobBankEntry struct { scale float32 } -func (e *knobBankEntry) lock(knob input.AnalogReader) { +func (e *knobBankEntry) lock(knob input.AnalogReader, lastValue float32) float32 { if e.locked { - return + return lastValue } - e.update(knob) e.locked = true + value := knob.ReadVoltage() + percent := knob.Percent() + return e.update(percent, value, lastValue) } func (e *knobBankEntry) unlock() { @@ -116,11 +129,24 @@ func (e *knobBankEntry) unlock() { e.locked = false } -func (e *knobBankEntry) update(knob input.AnalogReader) { +func (e *knobBankEntry) Percent() float32 { + return math.Lerp[float32](e.percent*e.scale, 0, 1) +} + +func (e *knobBankEntry) Value() float32 { + return math.Clamp(e.value*e.scale, e.minVoltage, e.maxVoltage) +} + +func (e *knobBankEntry) update(percent, value, lastValue float32) float32 { if !e.enabled || e.locked { - return + return lastValue + } + + if math.Abs(value-lastValue) < 0.05 { + return lastValue } - e.percent = math.Lerp[float32](knob.Percent()*e.scale, 0, 1) - e.value = math.Clamp(knob.ReadVoltage()*e.scale, e.minVoltage, e.maxVoltage) + e.percent = percent + e.value = value + return value } diff --git a/experimental/knobbankoptions.go b/experimental/knobbankoptions.go index 97f4e86..37bd93c 100644 --- a/experimental/knobbankoptions.go +++ b/experimental/knobbankoptions.go @@ -20,6 +20,7 @@ func WithLockedKnob(name string, opts ...KnobOption) KnobBankOption { e := knobBankEntry{ name: name, enabled: true, + locked: true, minVoltage: input.MinVoltage, maxVoltage: input.MaxVoltage, scale: 1, diff --git a/experimental/knoboptions.go b/experimental/knoboptions.go index cad1308..56c85e7 100644 --- a/experimental/knoboptions.go +++ b/experimental/knoboptions.go @@ -1,6 +1,11 @@ package experimental -import "fmt" +import ( + "fmt" + + "github.com/heucuva/europi/input" + "github.com/heucuva/europi/internal/math" +) type KnobOption func(e *knobBankEntry) error @@ -9,7 +14,9 @@ func InitialPercentageValue(v float32) KnobOption { if v < 0 || v > 1 { return fmt.Errorf("initial percentage value of %f is outside the range [0..1]", v) } + e.percent = v + e.value = math.Lerp[float32](v, input.MinVoltage, input.MaxVoltage) return nil } } diff --git a/internal/math/math.go b/internal/math/math.go index 6bf94ba..6379cc4 100644 --- a/internal/math/math.go +++ b/internal/math/math.go @@ -14,3 +14,12 @@ func Clamp[V Clampable](value, low, high V) V { } return value } + +// Abs returns the absolute value +func Abs(value float32) float32 { + if value >= 0 { + return value + } + + return -value +} From b42f234322861867c9ed836090654f17c7a75989 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 4 Apr 2023 09:25:54 -0700 Subject: [PATCH 07/62] More cleanup of bootstrap and layout - ripping out config/file support for now (needs complex cgo to solve) - adding on-screen panic logging support - adding on-screen logging support - adding CV and V/Oct value support - reducing required go version to minimum viable --- bootstrap.go | 18 ++++---- bootstrap_features.go | 19 ++++++++ bootstrap_panic.go | 37 +++++++++++++++ bootstrap_panicdisabled.go | 8 ++++ bootstrap_panicenabled.go | 8 ++++ bootstrapoptions.go | 4 +- bootstrapoptions_features.go | 10 ++++ config/setting.go | 46 ------------------- experimental/displaylogger/logger.go | 46 +++++++++++++++++++ experimental/{ => knobbank}/knobbank.go | 4 +- .../{ => knobbank}/knobbankoptions.go | 2 +- experimental/{ => knobbank}/knoboptions.go | 4 +- go.mod | 4 +- go.sum | 4 +- input/analog.go | 5 +- input/knob.go | 2 +- internal/projects/clockwerk/clockwerk.go | 6 +-- {internal/math => math}/lerp.go | 2 +- {internal/math => math}/math.go | 2 +- output/display.go | 5 ++ output/output.go | 16 +++++-- units/cv.go | 16 +++++++ units/units_debug.go | 14 ++++++ units/units_release.go | 7 +++ units/voct.go | 16 +++++++ 25 files changed, 228 insertions(+), 77 deletions(-) create mode 100644 bootstrap_features.go create mode 100644 bootstrap_panic.go create mode 100644 bootstrap_panicdisabled.go create mode 100644 bootstrap_panicenabled.go delete mode 100644 config/setting.go create mode 100644 experimental/displaylogger/logger.go rename experimental/{ => knobbank}/knobbank.go (97%) rename experimental/{ => knobbank}/knobbankoptions.go (97%) rename experimental/{ => knobbank}/knoboptions.go (86%) rename {internal/math => math}/lerp.go (70%) rename {internal/math => math}/math.go (87%) create mode 100644 units/cv.go create mode 100644 units/units_debug.go create mode 100644 units/units_release.go create mode 100644 units/voct.go diff --git a/bootstrap.go b/bootstrap.go index 18a1b76..e629345 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -2,11 +2,8 @@ package europi import ( "errors" - "fmt" "sync" "time" - - "github.com/heucuva/europi/output" ) var ( @@ -20,6 +17,7 @@ var ( func Bootstrap(options ...BootstrapOption) error { config := bootstrapConfig{ mainLoopInterval: DefaultMainLoopInterval, + panicHandler: DefaultPanicHandler, onPostBootstrapConstructionFn: DefaultPostBootstrapInitialization, onPreInitializeComponentsFn: nil, @@ -43,12 +41,10 @@ func Bootstrap(options ...BootstrapOption) error { Pi = e piWantDestroyChan = make(chan struct{}, 1) + panicHandler := config.panicHandler defer func() { - if err := recover(); err != nil { - fnt := output.DefaultFont - e.Display.SetFont(fnt) - e.Display.WriteLine(fmt.Sprint(err), 0, int16(fnt.YAdvance)) - _ = e.Display.Display() + if err := recover(); err != nil && panicHandler != nil { + panicHandler(e, err) } }() @@ -89,7 +85,9 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) { config.onPreInitializeComponentsFn(e) } - // TODO: initialize components + if config.enableDisplayLogger { + enableDisplayLogger(e) + } if config.onPostInitializeComponentsFn != nil { config.onPostInitializeComponentsFn(e) @@ -151,6 +149,8 @@ func bootstrapDestroy(config *bootstrapConfig, e *EuroPi) { config.onBeginDestroyFn(e) } + disableDisplayLogger(e) + close(piWantDestroyChan) Pi = nil diff --git a/bootstrap_features.go b/bootstrap_features.go new file mode 100644 index 0000000..5b47936 --- /dev/null +++ b/bootstrap_features.go @@ -0,0 +1,19 @@ +package europi + +import ( + "log" + "os" + + "github.com/heucuva/europi/experimental/displaylogger" +) + +func enableDisplayLogger(e *EuroPi) { + log.SetFlags(0) + log.SetOutput(&displaylogger.Logger{ + Display: e.Display, + }) +} + +func disableDisplayLogger(e *EuroPi) { + log.SetOutput(os.Stdout) +} diff --git a/bootstrap_panic.go b/bootstrap_panic.go new file mode 100644 index 0000000..b648403 --- /dev/null +++ b/bootstrap_panic.go @@ -0,0 +1,37 @@ +package europi + +import ( + "fmt" + "log" + + "github.com/heucuva/europi/output" +) + +// DefaultPanicHandler is the default handler for panics +// This will be set by the build flag `onscreenpanic` to `handlePanicOnScreenLog` +// Not setting the build flag will set it to `handlePanicDisplayCrash` +var DefaultPanicHandler func(e *EuroPi, err any) + +func handlePanicOnScreenLog(e *EuroPi, err any) { + if e == nil { + // can't do anything if it's not enabled + } + + // force display-logging to enabled + enableDisplayLogger(e) + + // show the panic on the screen + log.Panicln(fmt.Sprint(err)) +} + +func handlePanicDisplayCrash(e *EuroPi, err any) { + if e == nil { + // can't do anything if it's not enabled + } + + // display a diagonal line pattern through the screen to show that the EuroPi is crashed + ymax := int16(output.OLEDHeight) - 1 + for x := int16(0); x < output.OLEDWidth; x += 4 { + e.Display.DrawLine(x, 0, x+ymax, ymax, output.White) + } +} diff --git a/bootstrap_panicdisabled.go b/bootstrap_panicdisabled.go new file mode 100644 index 0000000..601c281 --- /dev/null +++ b/bootstrap_panicdisabled.go @@ -0,0 +1,8 @@ +//go:build !onscreenpanic +// +build !onscreenpanic + +package europi + +func init() { + DefaultPanicHandler = handlePanicDisplayCrash +} diff --git a/bootstrap_panicenabled.go b/bootstrap_panicenabled.go new file mode 100644 index 0000000..39bd62d --- /dev/null +++ b/bootstrap_panicenabled.go @@ -0,0 +1,8 @@ +//go:build onscreenpanic +// +build onscreenpanic + +package europi + +func init() { + DefaultPanicHandler = handlePanicOnScreenLog +} diff --git a/bootstrapoptions.go b/bootstrapoptions.go index 59b1c7b..214d313 100644 --- a/bootstrapoptions.go +++ b/bootstrapoptions.go @@ -8,7 +8,9 @@ import ( type BootstrapOption func(o *bootstrapConfig) error type bootstrapConfig struct { - mainLoopInterval time.Duration + mainLoopInterval time.Duration + panicHandler func(e *EuroPi, err any) + enableDisplayLogger bool // lifecycle callbacks onPostBootstrapConstructionFn PostBootstrapConstructionFunc diff --git a/bootstrapoptions_features.go b/bootstrapoptions_features.go index 30ee5dd..fe9b91c 100644 --- a/bootstrapoptions_features.go +++ b/bootstrapoptions_features.go @@ -15,3 +15,13 @@ func MainLoopInterval(interval time.Duration) BootstrapOption { return nil } } + +// EnableDisplayLogger enables (or disables) the logging of `log.Printf` (and similar) messages to +// the EuroPi's display. Enabling this will likely be undesirable except in cases where on-screen +// debugging is absoluely necessary. +func EnableDisplayLogger(enabled bool) BootstrapOption { + return func(o *bootstrapConfig) error { + o.enableDisplayLogger = enabled + return nil + } +} diff --git a/config/setting.go b/config/setting.go deleted file mode 100644 index 54f2eed..0000000 --- a/config/setting.go +++ /dev/null @@ -1,46 +0,0 @@ -package config - -import ( - "runtime/interrupt" - "runtime/volatile" - "unsafe" -) - -const ( - picoFlashStart = 0x10000000 -) - -type Setting[T any] struct { - Value T - initialized bool -} - -// Init initializes the setting with the expected default or loads the saved value from pico flash -func (s *Setting[T]) Init(value T) { - valuePtr := unsafe.Slice(&s.Value, 1) - vfp := unsafe.Add(unsafe.Pointer(&s.Value), picoFlashStart) - valueFlashPtr := unsafe.Slice((*T)(vfp), 1) - initializedFlashPtr := (*uint8)(unsafe.Add(unsafe.Pointer(&s.initialized), picoFlashStart)) - - state := interrupt.Disable() - if iv := volatile.LoadUint8(initializedFlashPtr); iv == 0 { - s.Value = value - } else { - copy(valuePtr, valueFlashPtr) - s.initialized = true - } - interrupt.Restore(state) -} - -// Flush flushes the value out to pico flash storage along with the initialize flag -func (s *Setting[T]) Flush() { - valuePtr := unsafe.Slice(&s.Value, 1) - vfp := unsafe.Add(unsafe.Pointer(&s.Value), picoFlashStart) - valueFlashPtr := unsafe.Slice((*T)(vfp), 1) - initializedFlashPtr := (*uint8)(unsafe.Add(unsafe.Pointer(&s.initialized), picoFlashStart)) - - state := interrupt.Disable() - copy(valueFlashPtr, valuePtr) - volatile.StoreUint8(initializedFlashPtr, 1) - interrupt.Restore(state) -} diff --git a/experimental/displaylogger/logger.go b/experimental/displaylogger/logger.go new file mode 100644 index 0000000..f46bfd4 --- /dev/null +++ b/experimental/displaylogger/logger.go @@ -0,0 +1,46 @@ +package displaylogger + +import ( + "strings" + + "github.com/heucuva/europi/output" +) + +type Logger struct { + sb strings.Builder + Display *output.Display +} + +func (w *Logger) repaint() { + str := w.sb.String() + + fnt := output.DefaultFont + w.Display.SetFont(fnt) + w.Display.ClearBuffer() + + lines := strings.Split(str, "\n") + w.sb.Reset() + _, maxY := w.Display.Size() + maxLines := (maxY + int16(fnt.YAdvance) - 1) / int16(fnt.YAdvance) + for l := len(lines); l > int(maxLines); l-- { + lines = lines[1:] + } + w.sb.WriteString(strings.Join(lines, "\n")) + + liney := fnt.YAdvance + for _, s := range lines { + w.Display.WriteLine(s, 0, int16(liney)) + liney += fnt.YAdvance + } + _ = w.Display.Display() +} + +func (w *Logger) Write(p []byte) (n int, err error) { + n, err = w.sb.Write(p) + if err != nil { + return + } + + w.repaint() + return +} diff --git a/experimental/knobbank.go b/experimental/knobbank/knobbank.go similarity index 97% rename from experimental/knobbank.go rename to experimental/knobbank/knobbank.go index 87c6607..791dd88 100644 --- a/experimental/knobbank.go +++ b/experimental/knobbank/knobbank.go @@ -1,8 +1,8 @@ -package experimental +package knobbank import ( "github.com/heucuva/europi/input" - "github.com/heucuva/europi/internal/math" + "github.com/heucuva/europi/math" ) type KnobBank struct { diff --git a/experimental/knobbankoptions.go b/experimental/knobbank/knobbankoptions.go similarity index 97% rename from experimental/knobbankoptions.go rename to experimental/knobbank/knobbankoptions.go index 37bd93c..f9f831d 100644 --- a/experimental/knobbankoptions.go +++ b/experimental/knobbank/knobbankoptions.go @@ -1,4 +1,4 @@ -package experimental +package knobbank import ( "fmt" diff --git a/experimental/knoboptions.go b/experimental/knobbank/knoboptions.go similarity index 86% rename from experimental/knoboptions.go rename to experimental/knobbank/knoboptions.go index 56c85e7..a6bf096 100644 --- a/experimental/knoboptions.go +++ b/experimental/knobbank/knoboptions.go @@ -1,10 +1,10 @@ -package experimental +package knobbank import ( "fmt" "github.com/heucuva/europi/input" - "github.com/heucuva/europi/internal/math" + "github.com/heucuva/europi/math" ) type KnobOption func(e *knobBankEntry) error diff --git a/go.mod b/go.mod index 5ceeebe..a787bd8 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/heucuva/europi -go 1.19 +go 1.18 require ( - tinygo.org/x/drivers v0.22.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..e599d43 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,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/input/analog.go b/input/analog.go index b0d57a2..1a70fa2 100644 --- a/input/analog.go +++ b/input/analog.go @@ -3,8 +3,7 @@ package input import ( "machine" - europiMath "github.com/heucuva/europi/internal/math" - europim "github.com/heucuva/europi/internal/math" + europim "github.com/heucuva/europi/math" ) const ( @@ -60,7 +59,7 @@ func (a *Analog) Choice(numItems int) int { func (a *Analog) read() uint16 { var sum int for i := 0; i < int(a.samples); i++ { - sum += europiMath.Clamp(int(a.Get())-CalibratedMinAI, 0, CalibratedMaxAI) + sum += europim.Clamp(int(a.Get())-CalibratedMinAI, 0, CalibratedMaxAI) } return uint16(sum / int(a.samples)) } diff --git a/input/knob.go b/input/knob.go index 29331c4..36fb044 100644 --- a/input/knob.go +++ b/input/knob.go @@ -4,7 +4,7 @@ import ( "machine" "math" - europim "github.com/heucuva/europi/internal/math" + europim "github.com/heucuva/europi/math" ) // A struct for handling the reading of knob voltage and position. diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index 5e978c6..fbd72f5 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -35,7 +35,7 @@ import ( "tinygo.org/x/tinydraw" "github.com/heucuva/europi" - europiMath "github.com/heucuva/europi/internal/math" + europim "github.com/heucuva/europi/math" "github.com/heucuva/europi/output" ) @@ -237,7 +237,7 @@ func main() { c.doClockReset = true return } - c.selected = uint8(europiMath.Clamp(int(c.selected)-1, 0, len(c.clocks))) + c.selected = uint8(europim.Clamp(int(c.selected)-1, 0, len(c.clocks))) c.displayShouldUpdate = true }) @@ -247,7 +247,7 @@ func main() { c.doClockReset = true return } - c.selected = uint8(europiMath.Clamp(int(c.selected)+1, 0, len(c.clocks)-1)) + c.selected = uint8(europim.Clamp(int(c.selected)+1, 0, len(c.clocks)-1)) c.displayShouldUpdate = true }) diff --git a/internal/math/lerp.go b/math/lerp.go similarity index 70% rename from internal/math/lerp.go rename to math/lerp.go index 086a454..f2a1e1f 100644 --- a/internal/math/lerp.go +++ b/math/lerp.go @@ -1,7 +1,7 @@ package math type Lerpable interface { - ~uint8 | ~uint16 | ~int | ~float32 + ~uint8 | ~uint16 | ~int | ~float32 | ~int32 | ~int64 } func Lerp[V Lerpable](t float32, low, high V) V { diff --git a/internal/math/math.go b/math/math.go similarity index 87% rename from internal/math/math.go rename to math/math.go index 6379cc4..2b59759 100644 --- a/internal/math/math.go +++ b/math/math.go @@ -1,7 +1,7 @@ package math type Clampable interface { - ~uint8 | ~uint16 | ~int | ~float32 + ~uint8 | ~uint16 | ~int | ~float32 | ~int32 | ~int64 } // Clamp returns a value that is no lower than "low" and no higher than "high". diff --git a/output/display.go b/output/display.go index d3e308d..7d3eeee 100644 --- a/output/display.go +++ b/output/display.go @@ -60,3 +60,8 @@ func (d *Display) WriteLine(text string, x, y int16) { func (d *Display) DrawHLine(x, y, xLen int16, c color.RGBA) { tinydraw.Line(d, x, y, x+xLen-1, y, c) } + +// DrawLine draws an arbitrary line +func (d *Display) DrawLine(x0, y0, x1, y1 int16, c color.RGBA) { + tinydraw.Line(d, x0, y0, x1, y1, c) +} diff --git a/output/output.go b/output/output.go index 1c7163d..cc7d231 100644 --- a/output/output.go +++ b/output/output.go @@ -4,7 +4,7 @@ import ( "log" "machine" - europiMath "github.com/heucuva/europi/internal/math" + europim "github.com/heucuva/europi/math" ) const ( @@ -29,6 +29,7 @@ type Output interface { Set(v bool) On() Off() + Voltage() float32 } // Output is struct for interacting with the cv output jacks. @@ -36,6 +37,7 @@ type output struct { pwm PWM pin machine.Pin ch uint8 + v float32 } // NewOutput returns a new Output interface. @@ -54,7 +56,7 @@ func NewOutput(pin machine.Pin, pwm PWM) Output { log.Fatal("pwm Channel error: ", err.Error()) } - return &output{pwm, pin, ch} + return &output{pwm, pin, ch, MinVoltage} } // Get returns the current set voltage in the range of 0 to pwm.Top(). @@ -73,19 +75,27 @@ func (o *output) Set(v bool) { // SetVoltage sets the current output voltage within a range of 0.0 to 10.0. func (o *output) SetVoltage(v float32) { - v = europiMath.Clamp(v, MinVoltage, MaxVoltage) + v = europim.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)) + o.v = v } // On sets the current voltage high at 10.0v. func (o *output) On() { + o.v = MaxVoltage 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) + o.v = MinVoltage +} + +// Voltage returns the current voltage +func (o *output) Voltage() float32 { + return o.v } diff --git a/units/cv.go b/units/cv.go new file mode 100644 index 0000000..e71901d --- /dev/null +++ b/units/cv.go @@ -0,0 +1,16 @@ +package units + +// 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 { + v := float32(c) + range_check(v, 0, 1, "cv") + return v * 5 +} + +// ToFloat32 returns a (normalized) CV value to its floating point representation [0.0 .. 1.0] +func (c CV) ToFloat32() float32 { + return float32(c) +} diff --git a/units/units_debug.go b/units/units_debug.go new file mode 100644 index 0000000..907d850 --- /dev/null +++ b/units/units_debug.go @@ -0,0 +1,14 @@ +//go:build debug +// +build debug + +package units + +import ( + "fmt" +) + +func range_check[T ~float32 | ~float64](v, min, max T, kind string) { + if v < min || v > max { + panic(fmt.Errorf("%w: %v", fmt.Errorf("%s out of range", kind), v)) + } +} diff --git a/units/units_release.go b/units/units_release.go new file mode 100644 index 0000000..0ddb37f --- /dev/null +++ b/units/units_release.go @@ -0,0 +1,7 @@ +//go:build !debug +// +build !debug + +package units + +func range_check[T ~float32 | ~float64](v, min, max T, kind string) { +} diff --git a/units/voct.go b/units/voct.go new file mode 100644 index 0000000..a77a81e --- /dev/null +++ b/units/voct.go @@ -0,0 +1,16 @@ +package units + +// 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 { + voct := float32(v) + range_check(voct, 0, 10, "v/oct") + return voct +} + +// ToFloat32 returns a V/Octave value to its floating point representation [0.0 .. 10.0] +func (v VOct) ToFloat32() float32 { + return float32(v) +} From eef6ad73c2ed3af077e14ce2248157a09c0d96dd Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 4 Apr 2023 09:41:03 -0700 Subject: [PATCH 08/62] Updating diagnostic and clockwerk apps - now using bootstrap system --- internal/projects/clockwerk/clockwerk.go | 70 +++++++------- internal/projects/diagnostics/diagnostics.go | 98 +++++++++++--------- 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index fbd72f5..c57e43b 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -215,56 +215,62 @@ func (c *Clockwerk) updateDisplay() { c.Display.Display() } -func main() { - c := Clockwerk{ - EuroPi: europi.New(), - clocks: DefaultFactor, - displayShouldUpdate: true, - } +var app Clockwerk + +func startLoop(e *europi.EuroPi) { + app.EuroPi = e + app.clocks = DefaultFactor + app.displayShouldUpdate = true // Lower range value can have lower sample size - c.K1.Samples(500) - c.K2.Samples(20) + app.K1.Samples(500) + app.K2.Samples(20) - c.DI.Handler(func(pin machine.Pin) { + app.DI.Handler(func(pin machine.Pin) { // Measure current period between clock pulses. - c.period = time.Now().Sub(c.DI.LastInput()) + app.period = time.Now().Sub(app.DI.LastInput()) }) // 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(p machine.Pin) { + if app.B2.Value() { + app.doClockReset = true return } - c.selected = uint8(europim.Clamp(int(c.selected)-1, 0, len(c.clocks))) - c.displayShouldUpdate = true + app.selected = uint8(europim.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(p machine.Pin) { + if app.B1.Value() { + app.doClockReset = true return } - c.selected = uint8(europim.Clamp(int(c.selected)+1, 0, len(c.clocks)-1)) - c.displayShouldUpdate = true + app.selected = uint8(europim.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() +} - for { - // Check for clock updates every 2 seconds. - time.Sleep(ResetDelay) - if c.doClockReset { - c.doClockReset = false - c.resetClocks() - c.displayShouldUpdate = true - } - europi.DebugMemoryUsage() +func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { + if app.doClockReset { + app.doClockReset = false + app.resetClocks() + app.displayShouldUpdate = true } + europi.DebugMemoryUsage() +} + +func main() { + europi.Bootstrap( + europi.StartLoop(startLoop), + europi.MainLoop(mainLoop), + europi.MainLoopInterval(ResetDelay), // Check for clock updates every 2 seconds. + ) } diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index c885430..778b754 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -4,6 +4,7 @@ package main import ( "fmt" "machine" + "time" "tinygo.org/x/tinydraw" @@ -20,13 +21,10 @@ type MyApp struct { prevStaticCv int } -func main() { - - myApp := MyApp{ - staticCv: 5, - } +var myApp MyApp - e := europi.New() +func startLoop(e *europi.EuroPi) { + myApp.staticCv = 5 // Demonstrate adding a IRQ handler to B1 and B2. e.B1.Handler(func(p machine.Pin) { @@ -36,46 +34,54 @@ func main() { e.B2.Handler(func(p machine.Pin) { myApp.staticCv = (myApp.staticCv + 1) % input.MaxVoltage }) +} + +func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { + e.Display.ClearBuffer() + + // Highlight the border of the oled display. + tinydraw.Rectangle(e.Display, 0, 0, 128, 32, output.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) - for { - e.Display.ClearBuffer() - - // Highlight the border of the oled display. - tinydraw.Rectangle(e.Display, 0, 0, 128, 32, output.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.SetVoltage(e.K1.ReadVoltage()) - e.CV4.SetVoltage(output.MaxVoltage - e.K1.ReadVoltage()) - myApp.prevK1 = e.K1.Range(1 << 12) - } - if e.K2.Range(1<<12) != myApp.prevK2 { - e.CV2.SetVoltage(e.K2.ReadVoltage()) - e.CV5.SetVoltage(output.MaxVoltage - e.K2.ReadVoltage()) - myApp.prevK2 = e.K2.Range(1 << 12) - } - e.CV3.On() - if myApp.staticCv != myApp.prevStaticCv { - e.CV6.SetVoltage(float32(myApp.staticCv)) - myApp.prevStaticCv = myApp.staticCv - } + e.Display.Display() + + // Set voltage values for the 6 CV outputs. + if e.K1.Range(1<<12) != myApp.prevK1 { + e.CV1.SetVoltage(e.K1.ReadVoltage()) + e.CV4.SetVoltage(output.MaxVoltage - e.K1.ReadVoltage()) + myApp.prevK1 = e.K1.Range(1 << 12) + } + if e.K2.Range(1<<12) != myApp.prevK2 { + e.CV2.SetVoltage(e.K2.ReadVoltage()) + e.CV5.SetVoltage(output.MaxVoltage - e.K2.ReadVoltage()) + myApp.prevK2 = e.K2.Range(1 << 12) } + e.CV3.On() + if myApp.staticCv != myApp.prevStaticCv { + e.CV6.SetVoltage(float32(myApp.staticCv)) + myApp.prevStaticCv = myApp.staticCv + } +} + +func main() { + europi.Bootstrap( + europi.StartLoop(startLoop), + europi.MainLoop(mainLoop), + europi.MainLoopInterval(time.Millisecond*1), + ) } From c0fea3f2b521a2d786062a605c4657a26f58b3eb Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 20 Apr 2023 08:41:34 -0700 Subject: [PATCH 09/62] Slimmer update with just bootstrap and 2 examples - large list of bootstrap changes and fixes - new knobmenu - new quantizer - new screenbank - button / digital input handlers now support rising and falling edges - text alignment on display - hertz unit (frequency domain) - lerpround and inverselerp math functions - clock generator example - random skips example --- bootstrap.go | 52 +++++-- bootstrap_features.go | 33 +++- bootstrap_lifecycle.go | 4 - bootstrap_panic.go | 17 ++- bootstrap_ui.go | 142 +++++++++++++++++ bootstrap_uimodule.go | 95 ++++++++++++ bootstrapoptions.go | 7 +- bootstrapoptions_features.go | 24 +++ bootstrapoptions_lifecycle.go | 2 +- bootstrapoptions_ui.go | 32 ++++ experimental/displaylogger/logger.go | 4 + experimental/knobbank/knobbank.go | 24 ++- experimental/knobmenu/item.go | 10 ++ experimental/knobmenu/knobmenu.go | 89 +++++++++++ experimental/knobmenu/options.go | 46 ++++++ experimental/quantizer/mode.go | 8 + experimental/quantizer/quantizer.go | 6 + experimental/quantizer/quantizer_round.go | 29 ++++ experimental/quantizer/quantizer_trunc.go | 29 ++++ experimental/screenbank/screenbank.go | 144 ++++++++++++++++++ experimental/screenbank/screenbankentry.go | 32 ++++ experimental/screenbank/screenbankoptions.go | 27 ++++ input/analog.go | 17 +++ input/analogreader.go | 4 + input/button.go | 72 ++++++--- input/digital.go | 74 ++++++--- input/digitalreader.go | 3 +- input/knob.go | 14 ++ internal/projects/clockgenerator/LICENSE.md | 11 ++ internal/projects/clockgenerator/README.md | 46 ++++++ .../projects/clockgenerator/clockgenerator.go | 66 ++++++++ .../clockgenerator/localtest/localtest.go | 93 +++++++++++ .../projects/clockgenerator/module/config.go | 14 ++ .../projects/clockgenerator/module/module.go | 119 +++++++++++++++ .../clockgenerator/module/setting_bpm.go | 25 +++ .../module/setting_gateduration.go | 25 +++ .../projects/clockgenerator/screen/main.go | 40 +++++ .../clockgenerator/screen/settings.go | 64 ++++++++ internal/projects/randomskips/LICENSE.md | 11 ++ internal/projects/randomskips/README.md | 57 +++++++ .../randomskips/localtest/localtest.go | 96 ++++++++++++ .../projects/randomskips/module/config.go | 6 + .../projects/randomskips/module/module.go | 65 ++++++++ .../randomskips/module/setting_chance.go | 20 +++ internal/projects/randomskips/randomskips.go | 92 +++++++++++ internal/projects/randomskips/screen/main.go | 42 +++++ .../projects/randomskips/screen/settings.go | 51 +++++++ math/lerp.go | 14 ++ output/alignment.go | 17 +++ output/display.go | 96 +++++++++++- output/output.go | 37 ++++- units/bipolarcv.go | 21 +++ units/cv.go | 5 + units/duration.go | 17 +++ units/hertz.go | 25 +++ units/voct.go | 5 + 56 files changed, 2131 insertions(+), 89 deletions(-) create mode 100644 bootstrap_ui.go create mode 100644 bootstrap_uimodule.go create mode 100644 bootstrapoptions_ui.go create mode 100644 experimental/knobmenu/item.go create mode 100644 experimental/knobmenu/knobmenu.go create mode 100644 experimental/knobmenu/options.go create mode 100644 experimental/quantizer/mode.go create mode 100644 experimental/quantizer/quantizer.go create mode 100644 experimental/quantizer/quantizer_round.go create mode 100644 experimental/quantizer/quantizer_trunc.go create mode 100644 experimental/screenbank/screenbank.go create mode 100644 experimental/screenbank/screenbankentry.go create mode 100644 experimental/screenbank/screenbankoptions.go create mode 100644 internal/projects/clockgenerator/LICENSE.md create mode 100644 internal/projects/clockgenerator/README.md create mode 100644 internal/projects/clockgenerator/clockgenerator.go create mode 100644 internal/projects/clockgenerator/localtest/localtest.go create mode 100644 internal/projects/clockgenerator/module/config.go create mode 100644 internal/projects/clockgenerator/module/module.go create mode 100644 internal/projects/clockgenerator/module/setting_bpm.go create mode 100644 internal/projects/clockgenerator/module/setting_gateduration.go create mode 100644 internal/projects/clockgenerator/screen/main.go create mode 100644 internal/projects/clockgenerator/screen/settings.go create mode 100644 internal/projects/randomskips/LICENSE.md create mode 100644 internal/projects/randomskips/README.md create mode 100644 internal/projects/randomskips/localtest/localtest.go create mode 100644 internal/projects/randomskips/module/config.go create mode 100644 internal/projects/randomskips/module/module.go create mode 100644 internal/projects/randomskips/module/setting_chance.go create mode 100644 internal/projects/randomskips/randomskips.go create mode 100644 internal/projects/randomskips/screen/main.go create mode 100644 internal/projects/randomskips/screen/settings.go create mode 100644 output/alignment.go create mode 100644 units/bipolarcv.go create mode 100644 units/duration.go create mode 100644 units/hertz.go diff --git a/bootstrap.go b/bootstrap.go index e629345..4477e87 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -16,8 +16,10 @@ var ( // Bootstrap will set up a global runtime environment (see europi.Pi) func Bootstrap(options ...BootstrapOption) error { config := bootstrapConfig{ - mainLoopInterval: DefaultMainLoopInterval, - panicHandler: DefaultPanicHandler, + mainLoopInterval: DefaultMainLoopInterval, + panicHandler: DefaultPanicHandler, + enableDisplayLogger: DefaultEnableDisplayLogger, + initRandom: DefaultInitRandom, onPostBootstrapConstructionFn: DefaultPostBootstrapInitialization, onPreInitializeComponentsFn: nil, @@ -41,17 +43,21 @@ func Bootstrap(options ...BootstrapOption) error { Pi = e piWantDestroyChan = make(chan struct{}, 1) - panicHandler := config.panicHandler - defer func() { - if err := recover(); err != nil && panicHandler != nil { - panicHandler(e, err) - } - }() - var onceBootstrapDestroy sync.Once + panicHandler := config.panicHandler + lastDestroyFunc := config.onBeginDestroyFn runBootstrapDestroy := func() { + reason := recover() + if reason != nil && panicHandler != nil { + config.onBeginDestroyFn = func(e *EuroPi, reason any) { + if lastDestroyFunc != nil { + lastDestroyFunc(e, reason) + } + panicHandler(e, reason) + } + } onceBootstrapDestroy.Do(func() { - bootstrapDestroy(&config, e) + bootstrapDestroy(&config, e, reason) }) } defer runBootstrapDestroy() @@ -89,6 +95,15 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) { enableDisplayLogger(e) } + if config.initRandom { + initRandom(e) + } + + // ui initializaiton is always last + if config.ui != nil { + enableUI(e, config.ui, config.uiRefreshRate) + } + if config.onPostInitializeComponentsFn != nil { config.onPostInitializeComponentsFn(e) } @@ -99,6 +114,10 @@ func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { config.onStartLoopFn(e) } + startUI(e) + + ForceRepaintUI(e) + if config.mainLoopInterval > 0 { bootstrapRunLoopWithDelay(config, e) } else { @@ -144,13 +163,22 @@ mainLoop: } } -func bootstrapDestroy(config *bootstrapConfig, e *EuroPi) { +func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, reason any) { if config.onBeginDestroyFn != nil { - config.onBeginDestroyFn(e) + config.onBeginDestroyFn(e, reason) } + disableUI(e) + disableDisplayLogger(e) + uninitRandom(e) + + if e != nil && e.Display != nil { + // show the last buffer + e.Display.Display() + } + close(piWantDestroyChan) Pi = nil diff --git a/bootstrap_features.go b/bootstrap_features.go index 5b47936..125ecb8 100644 --- a/bootstrap_features.go +++ b/bootstrap_features.go @@ -2,18 +2,47 @@ package europi import ( "log" + "machine" + "math/rand" "os" "github.com/heucuva/europi/experimental/displaylogger" ) +var ( + dispLog *displaylogger.Logger +) + func enableDisplayLogger(e *EuroPi) { + if dispLog != nil { + // already enabled - can happen when panicking + return + } + log.SetFlags(0) - log.SetOutput(&displaylogger.Logger{ + dispLog = &displaylogger.Logger{ Display: e.Display, - }) + } + log.SetOutput(dispLog) } func disableDisplayLogger(e *EuroPi) { + dispLog = nil log.SetOutput(os.Stdout) } + +func flushDisplayLogger(e *EuroPi) { + if dispLog != nil { + dispLog.Flush() + } +} + +func initRandom(e *EuroPi) { + xl, _ := machine.GetRNG() + xh, _ := machine.GetRNG() + x := int64(xh)<<32 | int64(xl) + rand.Seed(x) +} + +func uninitRandom(e *EuroPi) { +} diff --git a/bootstrap_lifecycle.go b/bootstrap_lifecycle.go index 34d6269..662e426 100644 --- a/bootstrap_lifecycle.go +++ b/bootstrap_lifecycle.go @@ -16,10 +16,6 @@ func DefaultBootstrapCompleted(e *EuroPi) { } } -const ( - DefaultMainLoopInterval time.Duration = time.Millisecond * 100 -) - // DefaultMainLoop is the default main loop used if a new one is not specified to Bootstrap() func DefaultMainLoop(e *EuroPi, deltaTime time.Duration) { } diff --git a/bootstrap_panic.go b/bootstrap_panic.go index b648403..b7d96ad 100644 --- a/bootstrap_panic.go +++ b/bootstrap_panic.go @@ -10,9 +10,9 @@ import ( // DefaultPanicHandler is the default handler for panics // This will be set by the build flag `onscreenpanic` to `handlePanicOnScreenLog` // Not setting the build flag will set it to `handlePanicDisplayCrash` -var DefaultPanicHandler func(e *EuroPi, err any) +var DefaultPanicHandler func(e *EuroPi, reason any) -func handlePanicOnScreenLog(e *EuroPi, err any) { +func handlePanicOnScreenLog(e *EuroPi, reason any) { if e == nil { // can't do anything if it's not enabled } @@ -21,17 +21,22 @@ func handlePanicOnScreenLog(e *EuroPi, err any) { enableDisplayLogger(e) // show the panic on the screen - log.Panicln(fmt.Sprint(err)) + log.Panicln(fmt.Sprint(reason)) } -func handlePanicDisplayCrash(e *EuroPi, err any) { +func handlePanicDisplayCrash(e *EuroPi, reason any) { if e == nil { // can't do anything if it's not enabled } // display a diagonal line pattern through the screen to show that the EuroPi is crashed ymax := int16(output.OLEDHeight) - 1 - for x := int16(0); x < output.OLEDWidth; x += 4 { - e.Display.DrawLine(x, 0, x+ymax, ymax, output.White) + for x := -ymax; x < output.OLEDWidth; x += 4 { + lx, ly := x, int16(0) + if x < 0 { + lx = 0 + ly = -x + } + e.Display.DrawLine(lx, ly, x+ymax, ymax, output.White) } } diff --git a/bootstrap_ui.go b/bootstrap_ui.go new file mode 100644 index 0000000..e972385 --- /dev/null +++ b/bootstrap_ui.go @@ -0,0 +1,142 @@ +package europi + +import ( + "machine" + "time" +) + +type UserInterface interface { + Start(e *EuroPi) + Paint(e *EuroPi, deltaTime time.Duration) +} + +type UserInterfaceLogoPainter interface { + PaintLogo(e *EuroPi, deltaTime time.Duration) +} + +type UserInterfaceButton1 interface { + Button1(e *EuroPi, p machine.Pin) +} + +type UserInterfaceButton1Debounce interface { + Button1Debounce() time.Duration +} + +type UserInterfaceButton1Ex interface { + Button1Ex(e *EuroPi, p machine.Pin, high bool) +} + +type UserInterfaceButton1Long interface { + Button1Long(e *EuroPi, p machine.Pin) +} + +type UserInterfaceButton2 interface { + Button2(e *EuroPi, p machine.Pin) +} + +type UserInterfaceButton2Debounce interface { + Button2Debounce() time.Duration +} + +type UserInterfaceButton2Ex interface { + Button2Ex(e *EuroPi, p machine.Pin, high bool) +} + +type UserInterfaceButton2Long interface { + Button2Long(e *EuroPi, p machine.Pin) +} + +var ( + ui uiModule +) + +func enableUI(e *EuroPi, screen UserInterface, interval time.Duration) { + ui.screen = screen + if ui.screen == nil { + return + } + + ui.logoPainter, _ = screen.(UserInterfaceLogoPainter) + + ui.repaint = make(chan struct{}, 1) + + var ( + inputB1 func(e *EuroPi, p machine.Pin, high bool) + inputB1L func(e *EuroPi, p machine.Pin) + ) + if in, ok := screen.(UserInterfaceButton1); ok { + var debounceDelay time.Duration + if db, ok := screen.(UserInterfaceButton1Debounce); ok { + debounceDelay = db.Button1Debounce() + } + var lastTrigger time.Time + inputB1 = func(e *EuroPi, p machine.Pin, high bool) { + now := time.Now() + if !high && (debounceDelay == 0 || now.Sub(lastTrigger) >= debounceDelay) { + lastTrigger = now + in.Button1(e, p) + } + } + } else if in, ok := screen.(UserInterfaceButton1Ex); ok { + inputB1 = in.Button1Ex + } + if in, ok := screen.(UserInterfaceButton1Long); ok { + inputB1L = in.Button1Long + } + ui.setupButton(e, e.B1, inputB1, inputB1L) + + var ( + inputB2 func(e *EuroPi, p machine.Pin, high bool) + inputB2L func(e *EuroPi, p machine.Pin) + ) + if in, ok := screen.(UserInterfaceButton2); ok { + var debounceDelay time.Duration + if db, ok := screen.(UserInterfaceButton2Debounce); ok { + debounceDelay = db.Button2Debounce() + } + var lastTrigger time.Time + inputB2 = func(e *EuroPi, p machine.Pin, high bool) { + now := time.Now() + if !high && (debounceDelay == 0 || now.Sub(lastTrigger) >= debounceDelay) { + lastTrigger = now + in.Button2(e, p) + } + } + } else if in, ok := screen.(UserInterfaceButton2Ex); ok { + inputB2 = in.Button2Ex + } + if in, ok := screen.(UserInterfaceButton2Long); ok { + inputB2L = in.Button2Long + } + ui.setupButton(e, e.B2, inputB2, inputB2L) + + ui.wg.Add(1) + go ui.run(e, interval) +} + +func startUI(e *EuroPi) { + if ui.screen == nil { + return + } + + ui.screen.Start(e) +} + +// ForceRepaintUI schedules a forced repaint of the UI (if it is configured and running) +func ForceRepaintUI(e *EuroPi) { + if ui.repaint != nil { + ui.repaint <- struct{}{} + } +} + +func disableUI(e *EuroPi) { + if ui.stop != nil { + ui.stop() + } + + if ui.repaint != nil { + close(ui.repaint) + } + + ui.wait() +} diff --git a/bootstrap_uimodule.go b/bootstrap_uimodule.go new file mode 100644 index 0000000..26773ca --- /dev/null +++ b/bootstrap_uimodule.go @@ -0,0 +1,95 @@ +package europi + +import ( + "context" + "machine" + "sync" + "time" + + "github.com/heucuva/europi/input" +) + +type uiModule struct { + screen UserInterface + logoPainter UserInterfaceLogoPainter + repaint chan struct{} + stop context.CancelFunc + wg sync.WaitGroup +} + +func (u *uiModule) wait() { + u.wg.Wait() +} + +func (u *uiModule) run(e *EuroPi, interval time.Duration) { + defer u.wg.Done() + + ctx, cancel := context.WithCancel(context.Background()) + ui.stop = cancel + defer ui.stop() + + t := time.NewTicker(interval) + defer t.Stop() + + disp := e.Display + lastTime := time.Now() + + paint := func(now time.Time) { + deltaTime := now.Sub(lastTime) + lastTime = now + disp.ClearBuffer() + if u.logoPainter != nil { + u.logoPainter.PaintLogo(e, deltaTime) + } + u.screen.Paint(e, deltaTime) + disp.Display() + } + + for { + select { + case <-ctx.Done(): + return + + case <-ui.repaint: + paint(time.Now()) + + case now := <-t.C: + paint(now) + } + } +} + +func (u *uiModule) setupButton(e *EuroPi, btn input.DigitalReader, onShort func(e *EuroPi, p machine.Pin, high bool), onLong func(e *EuroPi, p machine.Pin)) { + if onShort == nil && onLong == nil { + return + } + + if onShort == nil { + // no-op + onShort = func(e *EuroPi, p machine.Pin, high bool) {} + } + + // if no long-press handler present, just reuse short-press handler + if onLong == nil { + onLong = func(e *EuroPi, p machine.Pin) { + onShort(e, p, false) + } + } + + const longDuration = time.Millisecond * 650 + + btn.HandlerEx(machine.PinRising|machine.PinFalling, func(p machine.Pin) { + high := btn.Value() + if high { + onShort(e, p, high) + } else { + startDown := btn.LastChange() + deltaTime := time.Since(startDown) + if deltaTime < longDuration { + onShort(e, p, high) + } else { + onLong(e, p) + } + } + }) +} diff --git a/bootstrapoptions.go b/bootstrapoptions.go index 214d313..2cc24dd 100644 --- a/bootstrapoptions.go +++ b/bootstrapoptions.go @@ -9,8 +9,13 @@ type BootstrapOption func(o *bootstrapConfig) error type bootstrapConfig struct { mainLoopInterval time.Duration - panicHandler func(e *EuroPi, err any) + panicHandler func(e *EuroPi, reason any) enableDisplayLogger bool + initRandom bool + + // user interface + ui UserInterface + uiRefreshRate time.Duration // lifecycle callbacks onPostBootstrapConstructionFn PostBootstrapConstructionFunc diff --git a/bootstrapoptions_features.go b/bootstrapoptions_features.go index fe9b91c..3f56abf 100644 --- a/bootstrapoptions_features.go +++ b/bootstrapoptions_features.go @@ -5,6 +5,10 @@ import ( "time" ) +const ( + DefaultMainLoopInterval time.Duration = time.Millisecond * 100 +) + // MainLoopInterval sets the interval between calls to the configured main loop function func MainLoopInterval(interval time.Duration) BootstrapOption { return func(o *bootstrapConfig) error { @@ -16,6 +20,10 @@ func MainLoopInterval(interval time.Duration) BootstrapOption { } } +const ( + DefaultEnableDisplayLogger bool = false +) + // EnableDisplayLogger enables (or disables) the logging of `log.Printf` (and similar) messages to // the EuroPi's display. Enabling this will likely be undesirable except in cases where on-screen // debugging is absoluely necessary. @@ -25,3 +33,19 @@ func EnableDisplayLogger(enabled bool) BootstrapOption { return nil } } + +const ( + DefaultInitRandom bool = true +) + +// InitRandom enables (or disables) the initialization of the Go standard library's `rand` package +// Seed value. Disabling this will likely be undesirable except in cases where deterministic 'random' +// number generation is required, as the standard library `rand` package defaults to a seed of 1 +// instead of some pseudo-random number, like current time or thermal values. +// To generate a pseudo-random number for the random seed, the `machine.GetRNG` function is used. +func InitRandom(enabled bool) BootstrapOption { + return func(o *bootstrapConfig) error { + o.initRandom = enabled + return nil + } +} diff --git a/bootstrapoptions_lifecycle.go b/bootstrapoptions_lifecycle.go index 2e1ff2f..0d4c33f 100644 --- a/bootstrapoptions_lifecycle.go +++ b/bootstrapoptions_lifecycle.go @@ -56,7 +56,7 @@ type ( StartLoopFunc func(e *EuroPi) MainLoopFunc func(e *EuroPi, deltaTime time.Duration) EndLoopFunc func(e *EuroPi) - BeginDestroyFunc func(e *EuroPi) + BeginDestroyFunc func(e *EuroPi, reason any) FinishDestroyFunc func(e *EuroPi) ) diff --git a/bootstrapoptions_ui.go b/bootstrapoptions_ui.go new file mode 100644 index 0000000..154f258 --- /dev/null +++ b/bootstrapoptions_ui.go @@ -0,0 +1,32 @@ +package europi + +import ( + "errors" + "time" +) + +// UI sets the user interface handler interface +func UI(ui UserInterface) BootstrapOption { + return func(o *bootstrapConfig) error { + if ui == nil { + return errors.New("ui must not be nil") + } + o.ui = ui + return nil + } +} + +const ( + DefaultUIRefreshRate time.Duration = time.Millisecond * 100 +) + +// UIRefreshRate sets the interval of refreshes of the user interface +func UIRefreshRate(interval time.Duration) BootstrapOption { + return func(o *bootstrapConfig) error { + if interval <= 0 { + return errors.New("interval must be greater than 0") + } + o.uiRefreshRate = interval + return nil + } +} diff --git a/experimental/displaylogger/logger.go b/experimental/displaylogger/logger.go index f46bfd4..6d277bc 100644 --- a/experimental/displaylogger/logger.go +++ b/experimental/displaylogger/logger.go @@ -44,3 +44,7 @@ func (w *Logger) Write(p []byte) (n int, err error) { w.repaint() return } + +func (w *Logger) Flush() { + w.repaint() +} diff --git a/experimental/knobbank/knobbank.go b/experimental/knobbank/knobbank.go index 791dd88..a527188 100644 --- a/experimental/knobbank/knobbank.go +++ b/experimental/knobbank/knobbank.go @@ -3,6 +3,8 @@ package knobbank import ( "github.com/heucuva/europi/input" "github.com/heucuva/europi/math" + europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/units" ) type KnobBank struct { @@ -34,6 +36,10 @@ func (kb *KnobBank) CurrentName() string { return kb.bank[kb.current].name } +func (kb *KnobBank) CurrentIndex() int { + return kb.current +} + func (kb *KnobBank) Current() input.AnalogReader { return kb } @@ -54,6 +60,14 @@ func (kb *KnobBank) ReadVoltage() float32 { return cur.Value() } +func (kb *KnobBank) ReadCV() units.CV { + return units.CV(math.Clamp(kb.Percent(), 0.0, 1.0)) +} + +func (kb *KnobBank) ReadVOct() units.VOct { + return units.VOct(kb.ReadVoltage()) +} + func (kb *KnobBank) Percent() float32 { percent := kb.knob.Percent() if len(kb.bank) == 0 { @@ -79,8 +93,8 @@ func (kb *KnobBank) Choice(numItems int) int { value := kb.knob.ReadVoltage() percent := kb.knob.Percent() kb.lastValue = cur.update(percent, value, kb.lastValue) - idx := math.Lerp(cur.Percent(), 0, 2*numItems+1) / 2 - return math.Clamp(idx, 0, numItems-1) + idx := europim.Lerp(cur.Percent(), 0, 2*numItems+1) / 2 + return europim.Clamp(idx, 0, numItems-1) } func (kb *KnobBank) Next() { @@ -130,11 +144,11 @@ func (e *knobBankEntry) unlock() { } func (e *knobBankEntry) Percent() float32 { - return math.Lerp[float32](e.percent*e.scale, 0, 1) + return europim.Lerp[float32](e.percent*e.scale, 0, 1) } func (e *knobBankEntry) Value() float32 { - return math.Clamp(e.value*e.scale, e.minVoltage, e.maxVoltage) + return europim.Clamp(e.value*e.scale, e.minVoltage, e.maxVoltage) } func (e *knobBankEntry) update(percent, value, lastValue float32) float32 { @@ -142,7 +156,7 @@ func (e *knobBankEntry) update(percent, value, lastValue float32) float32 { return lastValue } - if math.Abs(value-lastValue) < 0.05 { + if europim.Abs(value-lastValue) < 0.05 { return lastValue } diff --git a/experimental/knobmenu/item.go b/experimental/knobmenu/item.go new file mode 100644 index 0000000..2009e9b --- /dev/null +++ b/experimental/knobmenu/item.go @@ -0,0 +1,10 @@ +package knobmenu + +import "github.com/heucuva/europi/units" + +type item struct { + name string + label string + stringFn func() string + updateFn func(value units.CV) +} diff --git a/experimental/knobmenu/knobmenu.go b/experimental/knobmenu/knobmenu.go new file mode 100644 index 0000000..34fffc0 --- /dev/null +++ b/experimental/knobmenu/knobmenu.go @@ -0,0 +1,89 @@ +package knobmenu + +import ( + "fmt" + "time" + + "github.com/heucuva/europi" + "github.com/heucuva/europi/experimental/knobbank" + "github.com/heucuva/europi/input" + europim "github.com/heucuva/europi/math" +) + +type KnobMenu struct { + kb *knobbank.KnobBank + items []item + selectedRune rune + unselectedRune rune + x int16 + y int16 + yadvance int16 +} + +func NewKnobMenu(knob input.AnalogReader, opts ...KnobMenuOption) (*KnobMenu, error) { + km := &KnobMenu{ + selectedRune: '*', + unselectedRune: ' ', + x: 0, + y: 11, + yadvance: 12, + } + + kbopts := []knobbank.KnobBankOption{ + knobbank.WithDisabledKnob(), + } + + for _, opt := range opts { + kbo, err := opt(km) + if err != nil { + return nil, err + } + + kbopts = append(kbopts, kbo...) + } + + kb, err := knobbank.NewKnobBank(knob, kbopts...) + if err != nil { + return nil, err + } + + km.kb = kb + + return km, nil +} + +func (m *KnobMenu) Next() { + m.kb.Next() +} + +func (m *KnobMenu) Paint(e *europi.EuroPi, deltaTime time.Duration) { + m.updateMenu(e) + + disp := e.Display + + y := m.y + selectedIdx := m.kb.CurrentIndex() - 1 + minI := europim.Clamp(selectedIdx-1, 0, len(m.items)-1) + maxI := europim.Clamp(minI+1, 0, len(m.items)-1) + for i := minI; i <= maxI && i < len(m.items); i++ { + it := &m.items[i] + + selRune := m.unselectedRune + if i == selectedIdx { + selRune = m.selectedRune + } + + disp.WriteLine(fmt.Sprintf("%c%s:%s", selRune, it.label, it.stringFn()), m.x, y) + y += m.yadvance + } +} + +func (m *KnobMenu) updateMenu(e *europi.EuroPi) { + cur := m.kb.CurrentName() + for _, it := range m.items { + if it.name == cur { + it.updateFn(m.kb.ReadCV()) + return + } + } +} diff --git a/experimental/knobmenu/options.go b/experimental/knobmenu/options.go new file mode 100644 index 0000000..fad7237 --- /dev/null +++ b/experimental/knobmenu/options.go @@ -0,0 +1,46 @@ +package knobmenu + +import ( + "fmt" + + "github.com/heucuva/europi/experimental/knobbank" + "github.com/heucuva/europi/units" +) + +type KnobMenuOption func(km *KnobMenu) ([]knobbank.KnobBankOption, error) + +func WithItem(name, label string, stringFn func() string, valueFn func() units.CV, updateFn func(value units.CV)) KnobMenuOption { + return func(km *KnobMenu) ([]knobbank.KnobBankOption, error) { + for _, it := range km.items { + if it.name == name { + return nil, fmt.Errorf("item %q already exists", name) + } + } + + km.items = append(km.items, item{ + name: name, + label: label, + stringFn: stringFn, + updateFn: updateFn, + }) + + return []knobbank.KnobBankOption{ + knobbank.WithLockedKnob(name, knobbank.InitialPercentageValue(valueFn().ToFloat32())), + }, nil + } +} + +func WithPosition(x, y int16) KnobMenuOption { + return func(km *KnobMenu) ([]knobbank.KnobBankOption, error) { + km.x = x + km.y = y + return nil, nil + } +} + +func WithYAdvance(yadvance int16) KnobMenuOption { + return func(km *KnobMenu) ([]knobbank.KnobBankOption, error) { + km.yadvance = yadvance + return nil, nil + } +} diff --git a/experimental/quantizer/mode.go b/experimental/quantizer/mode.go new file mode 100644 index 0000000..d441d00 --- /dev/null +++ b/experimental/quantizer/mode.go @@ -0,0 +1,8 @@ +package quantizer + +type Mode int + +const ( + ModeRound = Mode(iota) + ModeTrunc +) diff --git a/experimental/quantizer/quantizer.go b/experimental/quantizer/quantizer.go new file mode 100644 index 0000000..77487d6 --- /dev/null +++ b/experimental/quantizer/quantizer.go @@ -0,0 +1,6 @@ +package quantizer + +type Quantizer[T any] interface { + QuantizeToIndex(in float32, length int) int + QuantizeToValue(in float32, list []T) T +} diff --git a/experimental/quantizer/quantizer_round.go b/experimental/quantizer/quantizer_round.go new file mode 100644 index 0000000..6e5b72a --- /dev/null +++ b/experimental/quantizer/quantizer_round.go @@ -0,0 +1,29 @@ +package quantizer + +import ( + "math" + + europim "github.com/heucuva/europi/math" +) + +type Round[T any] struct{} + +func (Round[T]) QuantizeToIndex(in float32, length int) int { + if length == 0 { + return -1 + } + + idx := int(math.Round(float64(length-1) * float64(in))) + idx = europim.Clamp(idx, 0, length-1) + return idx +} + +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/experimental/quantizer/quantizer_trunc.go b/experimental/quantizer/quantizer_trunc.go new file mode 100644 index 0000000..4387e71 --- /dev/null +++ b/experimental/quantizer/quantizer_trunc.go @@ -0,0 +1,29 @@ +package quantizer + +import ( + "math" + + europim "github.com/heucuva/europi/math" +) + +type Trunc[T any] struct{} + +func (Trunc[T]) QuantizeToIndex(in float32, length int) int { + if length == 0 { + return -1 + } + + idx := int(math.Trunc(float64(length-1) * float64(in))) + idx = europim.Clamp(idx, 0, length-1) + return idx +} + +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/experimental/screenbank/screenbank.go b/experimental/screenbank/screenbank.go new file mode 100644 index 0000000..35558b0 --- /dev/null +++ b/experimental/screenbank/screenbank.go @@ -0,0 +1,144 @@ +package screenbank + +import ( + "machine" + "time" + + "github.com/heucuva/europi" + "github.com/heucuva/europi/output" +) + +type ScreenBank struct { + screen europi.UserInterface + current int + bank []screenBankEntry +} + +func NewScreenBank(opts ...ScreenBankOption) (*ScreenBank, error) { + sb := &ScreenBank{} + + for _, opt := range opts { + if err := opt(sb); err != nil { + return nil, err + } + } + + return sb, nil +} + +func (sb *ScreenBank) CurrentName() string { + if len(sb.bank) == 0 { + return "" + } + return sb.bank[sb.current].name +} + +func (sb *ScreenBank) Current() europi.UserInterface { + if len(sb.bank) == 0 { + return nil + } + return sb.bank[sb.current].screen +} + +func (sb *ScreenBank) transitionTo(idx int) { + if sb.current >= len(sb.bank) || len(sb.bank) == 0 { + return + } + + cur := sb.bank[sb.current] + cur.lock() + sb.current = idx + if sb.current >= len(sb.bank) { + sb.current = 0 + } + sb.bank[sb.current].unlock() +} + +func (sb *ScreenBank) Goto(idx int) { + sb.transitionTo(idx) +} + +func (sb *ScreenBank) GotoNamed(name string) { + for i, screen := range sb.bank { + if screen.name == name { + sb.transitionTo(i) + return + } + } +} + +func (sb *ScreenBank) Next() { + sb.transitionTo(sb.current + 1) +} + +func (sb *ScreenBank) Start(e *europi.EuroPi) { + for i := range sb.bank { + s := &sb.bank[i] + + s.lock() + s.screen.Start(e) + s.lastUpdate = time.Now() + s.unlock() + } +} + +func (sb *ScreenBank) PaintLogo(e *europi.EuroPi, deltaTime time.Duration) { + if sb.current >= len(sb.bank) { + return + } + + cur := &sb.bank[sb.current] + cur.lock() + if cur.logo != "" { + e.Display.WriteEmojiLineInverseAligned(cur.logo, 0, 16, output.AlignRight, output.AlignMiddle) + } + cur.unlock() +} + +func (sb *ScreenBank) Paint(e *europi.EuroPi, deltaTime time.Duration) { + if sb.current >= len(sb.bank) { + return + } + + cur := &sb.bank[sb.current] + cur.lock() + now := time.Now() + cur.screen.Paint(e, now.Sub(cur.lastUpdate)) + cur.lastUpdate = now + cur.unlock() +} + +func (sb *ScreenBank) Button1Ex(e *europi.EuroPi, p machine.Pin, high bool) { + screen := sb.Current() + if cur, ok := screen.(europi.UserInterfaceButton1); ok { + if !high { + cur.Button1(e, p) + } + } else if cur, ok := screen.(europi.UserInterfaceButton1Ex); ok { + cur.Button1Ex(e, p, high) + } +} + +func (sb *ScreenBank) Button1Long(e *europi.EuroPi, p machine.Pin) { + if cur, ok := sb.Current().(europi.UserInterfaceButton1Long); ok { + cur.Button1Long(e, p) + } else { + // try the short-press + sb.Button1Ex(e, p, false) + } +} + +func (sb *ScreenBank) Button2Ex(e *europi.EuroPi, p machine.Pin, high bool) { + screen := sb.Current() + if cur, ok := screen.(europi.UserInterfaceButton2); ok { + if !high { + cur.Button2(e, p) + } + } else if cur, ok := screen.(europi.UserInterfaceButton2Ex); ok { + cur.Button2Ex(e, p, high) + } +} + +func (sb *ScreenBank) Button2Long(e *europi.EuroPi, p machine.Pin) { + sb.Next() +} diff --git a/experimental/screenbank/screenbankentry.go b/experimental/screenbank/screenbankentry.go new file mode 100644 index 0000000..0d1d29c --- /dev/null +++ b/experimental/screenbank/screenbankentry.go @@ -0,0 +1,32 @@ +package screenbank + +import ( + "time" + + "github.com/heucuva/europi" +) + +type screenBankEntry struct { + name string + logo string + screen europi.UserInterface + enabled bool + locked bool + lastUpdate time.Time +} + +func (e *screenBankEntry) lock() { + if e.locked { + return + } + + e.locked = true +} + +func (e *screenBankEntry) unlock() { + if !e.enabled { + return + } + + e.locked = false +} diff --git a/experimental/screenbank/screenbankoptions.go b/experimental/screenbank/screenbankoptions.go new file mode 100644 index 0000000..39a9dc8 --- /dev/null +++ b/experimental/screenbank/screenbankoptions.go @@ -0,0 +1,27 @@ +package screenbank + +import ( + "time" + + "github.com/heucuva/europi" +) + +type ScreenBankOption func(sb *ScreenBank) error + +// WithScreen sets up a new screen in the chain +// logo is the emoji to use (see https://github.com/tinygo-org/tinyfont/blob/release/notoemoji/NotoEmoji-Regular-12pt.go) +func WithScreen(name string, logo string, screen europi.UserInterface) ScreenBankOption { + return func(sb *ScreenBank) error { + e := screenBankEntry{ + name: name, + logo: logo, + screen: screen, + enabled: true, + locked: true, + lastUpdate: time.Now(), + } + + sb.bank = append(sb.bank, e) + return nil + } +} diff --git a/input/analog.go b/input/analog.go index 1a70fa2..9488cf0 100644 --- a/input/analog.go +++ b/input/analog.go @@ -2,8 +2,10 @@ package input import ( "machine" + "runtime/interrupt" europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/units" ) const ( @@ -47,6 +49,19 @@ func (a *Analog) ReadVoltage() float32 { return a.Percent() * MaxVoltage } +// ReadCV returns the current read voltage as a CV value. +func (a *Analog) ReadCV() units.CV { + // we can't use a.Percent() here, because we might get over 5.0 volts input + // just clamp it + v := a.ReadVoltage() + return units.CV(europim.Clamp(v/5.0, 0.0, 1.0)) +} + +// ReadCV returns the current read voltage as a V/Octave value. +func (a *Analog) ReadVOct() units.VOct { + return units.VOct(a.ReadVoltage()) +} + // Range return a value between 0 and the given steps (not inclusive) based on the range of the analog input. func (a *Analog) Range(steps uint16) uint16 { return uint16(a.Percent() * float32(steps)) @@ -58,8 +73,10 @@ func (a *Analog) Choice(numItems int) int { func (a *Analog) read() uint16 { var sum int + state := interrupt.Disable() for i := 0; i < int(a.samples); i++ { sum += europim.Clamp(int(a.Get())-CalibratedMinAI, 0, CalibratedMaxAI) } + interrupt.Restore(state) return uint16(sum / int(a.samples)) } diff --git a/input/analogreader.go b/input/analogreader.go index 9c540de..cf0afea 100644 --- a/input/analogreader.go +++ b/input/analogreader.go @@ -2,12 +2,16 @@ package input import ( "machine" + + "github.com/heucuva/europi/units" ) // AnalogReader is an interface for common analog read methods for knobs and cv input. type AnalogReader interface { Samples(samples uint16) ReadVoltage() float32 + ReadCV() units.CV + ReadVOct() units.VOct Percent() float32 Range(steps uint16) uint16 Choice(numItems int) int diff --git a/input/button.go b/input/button.go index 9ec1c55..c45ef64 100644 --- a/input/button.go +++ b/input/button.go @@ -2,55 +2,81 @@ package input import ( "machine" + "runtime/interrupt" "time" ) // 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) + Pin machine.Pin + lastChange time.Time } // 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, + Pin: pin, + lastChange: time.Now(), } } // 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) + if handler == nil { + panic("cannot set nil handler") + } + b.HandlerEx(machine.PinRising|machine.PinFalling, func(p machine.Pin) { + if b.Value() { + handler(p) + } + }) } -// 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) +// HandlerEx sets the callback function to be call when the button changes in a specified way. +func (b *Button) HandlerEx(pinChange machine.PinChange, handler func(p machine.Pin)) { + if handler == nil { + panic("cannot set nil handler") + } + b.setHandler(pinChange, handler) } -func (b *Button) debounceWrapper(p machine.Pin) { - t := time.Now() - if t.Before(b.lastInput.Add(b.debounceDelay)) { - return +// HandlerWithDebounce 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) { + if handler == nil { + panic("cannot set nil handler") } - b.callback(p) - b.lastInput = t + lastInput := time.Now() + b.Handler(func(p machine.Pin) { + now := time.Now() + if now.Before(lastInput.Add(delay)) { + return + } + handler(p) + lastInput = now + }) +} + +func (b *Button) setHandler(pinChange machine.PinChange, handler func(p machine.Pin)) { + state := interrupt.Disable() + b.Pin.SetInterrupt(pinChange, func(p machine.Pin) { + now := time.Now() + handler(p) + b.lastChange = now + }) + interrupt.Restore(state) } -// LastInput return the time of the last button press. -func (b *Button) LastInput() time.Time { - return b.lastInput +// LastChange return the time of the last button input change. +func (b *Button) LastChange() time.Time { + return b.lastChange } // Value returns true if button is currently pressed, else false. func (b *Button) Value() bool { + state := interrupt.Disable() // Invert signal to match expected behavior. - return !b.Pin.Get() + v := !b.Pin.Get() + interrupt.Restore(state) + return v } diff --git a/input/digital.go b/input/digital.go index c23e77b..7df136d 100644 --- a/input/digital.go +++ b/input/digital.go @@ -2,6 +2,7 @@ package input import ( "machine" + "runtime/interrupt" "time" ) @@ -9,50 +10,75 @@ const DefaultDebounceDelay = time.Duration(50 * time.Millisecond) // Digital is a struct for handling reading of the digital input. type Digital struct { - Pin machine.Pin - debounceDelay time.Duration - lastInput time.Time - callback func(p machine.Pin) + Pin machine.Pin + lastChange time.Time } // NewDigital creates a new Digital struct. func NewDigital(pin machine.Pin) *Digital { pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) return &Digital{ - Pin: pin, - lastInput: time.Now(), - debounceDelay: DefaultDebounceDelay, + Pin: pin, + lastChange: time.Now(), } } -// LastInput return the time of the last high input (triggered at 0.8v). -func (d *Digital) LastInput() time.Time { - return d.lastInput +// LastChange return the time of the last input change (triggered at 0.8v). +func (d *Digital) LastChange() time.Time { + return d.lastChange } // Value returns true if the input is high (above 0.8v), else false. func (d *Digital) Value() bool { + state := interrupt.Disable() // Invert signal to match expected behavior. - return !d.Pin.Get() + v := !d.Pin.Get() + interrupt.Restore(state) + return v } -// Handler sets the callback function to be call when a rising edge is detected. +// Handler sets the callback function to be call when the falling edge is detected. func (d *Digital) Handler(handler func(p machine.Pin)) { - d.HandlerWithDebounce(handler, 0) + if handler == nil { + panic("cannot set nil handler") + } + d.HandlerEx(machine.PinRising|machine.PinFalling, func(p machine.Pin) { + if d.Value() { + handler(p) + } + }) } -// Handler sets the callback function to be call when a rising edge is detected and debounce delay time has elapsed. -func (d *Digital) HandlerWithDebounce(handler func(p machine.Pin), delay time.Duration) { - d.callback = handler - d.debounceDelay = delay - d.Pin.SetInterrupt(machine.PinFalling, d.debounceWrapper) +// HandlerEx sets the callback function to be call when the input changes in a specified way. +func (d *Digital) HandlerEx(pinChange machine.PinChange, handler func(p machine.Pin)) { + if handler == nil { + panic("cannot set nil handler") + } + d.setHandler(pinChange, handler) } -func (d *Digital) debounceWrapper(p machine.Pin) { - t := time.Now() - if t.Before(d.lastInput.Add(d.debounceDelay)) { - return +// HandlerWithDebounce sets the callback function to be call when the falling edge is detected and debounce delay time has elapsed. +func (d *Digital) HandlerWithDebounce(handler func(p machine.Pin), delay time.Duration) { + if handler == nil { + panic("cannot set nil handler") } - d.callback(p) - d.lastInput = t + lastInput := time.Now() + d.Handler(func(p machine.Pin) { + now := time.Now() + if now.Before(lastInput.Add(delay)) { + return + } + handler(p) + lastInput = now + }) +} + +func (d *Digital) setHandler(pinChange machine.PinChange, handler func(p machine.Pin)) { + state := interrupt.Disable() + d.Pin.SetInterrupt(pinChange, func(p machine.Pin) { + now := time.Now() + handler(p) + d.lastChange = now + }) + interrupt.Restore(state) } diff --git a/input/digitalreader.go b/input/digitalreader.go index 9c1d50a..04807d1 100644 --- a/input/digitalreader.go +++ b/input/digitalreader.go @@ -8,7 +8,8 @@ import ( // DigitalReader is an interface for common digital inputs methods. type DigitalReader interface { Handler(func(machine.Pin)) + HandlerEx(machine.PinChange, func(machine.Pin)) HandlerWithDebounce(func(machine.Pin), time.Duration) - LastInput() time.Time + LastChange() time.Time Value() bool } diff --git a/input/knob.go b/input/knob.go index 36fb044..1706a7a 100644 --- a/input/knob.go +++ b/input/knob.go @@ -3,8 +3,10 @@ package input import ( "machine" "math" + "runtime/interrupt" europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/units" ) // A struct for handling the reading of knob voltage and position. @@ -35,6 +37,16 @@ func (k *Knob) ReadVoltage() float32 { return k.Percent() * MaxVoltage } +// ReadCV returns the current read voltage as a CV value. +func (k *Knob) ReadCV() units.CV { + return units.CV(k.Percent()) +} + +// ReadCV returns the current read voltage as a V/Octave value. +func (k *Knob) ReadVOct() units.VOct { + return units.VOct(k.ReadVoltage()) +} + // 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)) @@ -46,8 +58,10 @@ func (k *Knob) Choice(numItems int) int { func (k *Knob) read() uint16 { var sum int + state := interrupt.Disable() for i := 0; i < int(k.samples); i++ { sum += int(k.Get()) } + interrupt.Restore(state) return uint16(sum / int(k.samples)) } diff --git a/internal/projects/clockgenerator/LICENSE.md b/internal/projects/clockgenerator/LICENSE.md new file mode 100644 index 0000000..b7aa0e2 --- /dev/null +++ b/internal/projects/clockgenerator/LICENSE.md @@ -0,0 +1,11 @@ +# Released under MIT License + +Copyright (c) 2023 Jason Crawford + +Portions Copyright (c) 2022 Adam Wonak + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/internal/projects/clockgenerator/README.md b/internal/projects/clockgenerator/README.md new file mode 100644 index 0000000..1d67318 --- /dev/null +++ b/internal/projects/clockgenerator/README.md @@ -0,0 +1,46 @@ +# Clock Generator + +A simple gate/clock generator based on YouTube video observations made about the operation of the ALM Pamela's NEW Workout module. + +## Scope of This App + +The scope of this app is to drive the CV 1 output as a gate output. + +### Outputs + +- CV 1 = Gate output + +## Using Clock Generator + +### Changing Screens + +Long-pressing (>=650ms) Button 2 on the EuroPi will transition to the next display in the chain. If you transition past the last item in the display chain, then the display will cycle to the first item. + +The order of the displays is: +- Main display +- Clock Generator configuration + +#### Main Display + +The main display shows the voltages of the CV outputs on the EuroPi as well as the enabled status of the Clock Generator. + +While Clock Generator is operating, you can toggle its activation mode (default mode at startup is `on`) by pressing Button 1 on the EuroPi while on the main screen. When the clock is active, you will be informed by seeing a small bar ( `_` ) in the upper-left corner of the display. + +#### Clock Generator Configuration + +By default, the settings of Clock Generator are: +- BPM: 120.0 +- Gate Duration: 100.0 ms + + +When on the Clock Generator Configuration screen, pressing Button 1 on the EuroPi will cycle through the configuration items. The currently selected item for edit will be identified by an asterisk (`*`) character and it may be updated by turning Knob 1 of the EuroPi. Updates are applied immediately. + +## Special Thanks + +- Adam Wonak +- Charlotte Cox +- Allen Synthesis +- ALM +- Mouser Electronics +- Waveshare Electronics +- Raspberry Pi Foundation diff --git a/internal/projects/clockgenerator/clockgenerator.go b/internal/projects/clockgenerator/clockgenerator.go new file mode 100644 index 0000000..1ea4a67 --- /dev/null +++ b/internal/projects/clockgenerator/clockgenerator.go @@ -0,0 +1,66 @@ +package main + +import ( + "time" + + "github.com/heucuva/europi" + "github.com/heucuva/europi/experimental/screenbank" + "github.com/heucuva/europi/internal/projects/clockgenerator/module" + "github.com/heucuva/europi/internal/projects/clockgenerator/screen" +) + +var ( + clock module.ClockGenerator + ui *screenbank.ScreenBank + screenMain = screen.Main{ + Clock: &clock, + } + screenSettings = screen.Settings{ + Clock: &clock, + } +) + +func startLoop(e *europi.EuroPi) { + if err := clock.Init(module.Config{ + BPM: 120.0, + GateDuration: time.Millisecond * 100, + Enabled: true, + ClockOut: func(high bool) { + if high { + e.CV1.On() + } else { + e.CV1.Off() + } + europi.ForceRepaintUI(e) + }, + }); err != nil { + panic(err) + } +} + +func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { + clock.Tick(deltaTime) +} + +func main() { + var err error + ui, err = screenbank.NewScreenBank( + screenbank.WithScreen("main", "\u2b50", &screenMain), + screenbank.WithScreen("settings", "\u2611", &screenSettings), + ) + if err != nil { + panic(err) + } + + // some options shown below are being explicitly set to their defaults + // only to showcase their existence. + europi.Bootstrap( + europi.EnableDisplayLogger(false), + europi.InitRandom(true), + europi.StartLoop(startLoop), + europi.MainLoop(mainLoop), + europi.MainLoopInterval(time.Millisecond*1), + europi.UI(ui), + europi.UIRefreshRate(time.Millisecond*50), + ) +} diff --git a/internal/projects/clockgenerator/localtest/localtest.go b/internal/projects/clockgenerator/localtest/localtest.go new file mode 100644 index 0000000..da580a1 --- /dev/null +++ b/internal/projects/clockgenerator/localtest/localtest.go @@ -0,0 +1,93 @@ +package main + +import ( + "fmt" + "math/rand" + "time" + + "github.com/heucuva/europi/internal/projects/clockgenerator/module" + "github.com/heucuva/europi/units" +) + +var ( + clockgenerator module.ClockGenerator + + outMap map[string]float32 = make(map[string]float32) +) + +func reportVolts(name string, v float32) { + switch name { + //case "cv1": + case "cv2": + case "cv3": + case "cv4": + case "cv5": + case "cv6": + default: + old := outMap[name] + if old != v { + fmt.Printf("%s: %v Volts\n", name, v) + outMap[name] = v + } + } +} + +func panicVOct(name string) func(units.VOct) { + return func(voct units.VOct) { + v := voct.ToVolts() + reportVolts(name, v) + } +} + +func panicCV(name string) func(units.CV) { + return func(cv units.CV) { + v := cv.ToVolts() + reportVolts(name, v) + } +} + +func bipolarOut(out func(units.CV)) func(cv units.BipolarCV) { + return func(cv units.BipolarCV) { + out(cv.ToCV()) + } +} + +func startLoop() { + setCV1 := panicCV("cv1") + + if err := clockgenerator.Init(module.Config{ + ClockOut: func(high bool) { + if high { + setCV1(1) + } else { + setCV1(0) + } + }, + BPM: 120.0, + GateDuration: time.Millisecond * 100, + Enabled: true, + }); err != nil { + panic(err) + } +} + +func mainLoop(deltaTime time.Duration) { + clockgenerator.Tick(deltaTime) +} + +func main() { + startLoop() + + ticker := time.NewTicker(time.Millisecond * 1) + defer ticker.Stop() + prev := time.Now() + for { + now := <-ticker.C + mainLoop(now.Sub(prev)) + prev = now + } +} + +func init() { + rand.Seed(time.Now().Unix()) +} diff --git a/internal/projects/clockgenerator/module/config.go b/internal/projects/clockgenerator/module/config.go new file mode 100644 index 0000000..3b5a2c0 --- /dev/null +++ b/internal/projects/clockgenerator/module/config.go @@ -0,0 +1,14 @@ +package module + +import "time" + +const ( + DefaultGateDuration = time.Millisecond * 100 +) + +type Config struct { + BPM float32 + GateDuration time.Duration + Enabled bool + ClockOut func(high bool) +} diff --git a/internal/projects/clockgenerator/module/module.go b/internal/projects/clockgenerator/module/module.go new file mode 100644 index 0000000..f5ac81f --- /dev/null +++ b/internal/projects/clockgenerator/module/module.go @@ -0,0 +1,119 @@ +package module + +import ( + "fmt" + "time" + + europim "github.com/heucuva/europi/math" +) + +type ClockGenerator struct { + interval time.Duration + gateDuration time.Duration + enabled bool + clockOut func(high bool) + t time.Duration + gateT time.Duration + gateLevel bool + + bpm float32 // informational +} + +func (m *ClockGenerator) Init(config Config) error { + fnClockOut := config.ClockOut + if fnClockOut == nil { + fnClockOut = noopClockOut + } + m.clockOut = fnClockOut + + m.bpm = config.BPM + if config.BPM <= 0 { + return fmt.Errorf("invalid bpm setting: %v", config.BPM) + } + m.gateDuration = config.GateDuration + if m.gateDuration == 0 { + m.gateDuration = DefaultGateDuration + } + m.enabled = config.Enabled + + m.SetBPM(config.BPM) + return nil +} + +func noopClockOut(high bool) { +} + +func (m *ClockGenerator) Toggle() { + m.enabled = !m.enabled + m.t = 0 +} + +func (m *ClockGenerator) SetEnabled(enabled bool) { + m.enabled = enabled + m.t = 0 +} + +func (m *ClockGenerator) Enabled() bool { + return m.enabled +} + +func (m *ClockGenerator) SetBPM(bpm float32) { + if bpm == 0 { + bpm = 120.0 + } + m.bpm = bpm + m.interval = time.Duration(float32(time.Minute) / bpm) +} + +func (m *ClockGenerator) BPM() float32 { + return m.bpm +} + +func (m *ClockGenerator) SetGateDuration(dur time.Duration) { + if dur == 0 { + dur = DefaultGateDuration + } + + m.gateDuration = europim.Clamp(dur, time.Microsecond, m.interval-time.Microsecond) +} + +func (m *ClockGenerator) GateDuration() time.Duration { + return m.gateDuration +} + +func (m *ClockGenerator) Tick(deltaTime time.Duration) { + if !m.enabled { + return + } + + prevGateLevel := m.gateLevel + + var reset bool + deltaTime, reset = m.processClockInterval(deltaTime) + + if reset { + m.gateT = 0 + m.gateLevel = true + } + + gateT := m.gateT + deltaTime + m.gateT = gateT % m.gateDuration + if gateT >= m.gateDuration { + m.gateLevel = false + } + + if m.gateLevel != prevGateLevel { + m.clockOut(m.gateLevel) + } +} + +func (m *ClockGenerator) processClockInterval(deltaTime time.Duration) (time.Duration, bool) { + t := m.t + deltaTime + m.t = t % m.interval + + if t >= m.interval { + return m.t, true + } + + return deltaTime, false +} diff --git a/internal/projects/clockgenerator/module/setting_bpm.go b/internal/projects/clockgenerator/module/setting_bpm.go new file mode 100644 index 0000000..9d40cb1 --- /dev/null +++ b/internal/projects/clockgenerator/module/setting_bpm.go @@ -0,0 +1,25 @@ +package module + +import ( + "fmt" + + europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/units" +) + +const ( + MinBPM float32 = 0.1 + MaxBPM float32 = 480.0 +) + +func BPMString(bpm float32) string { + return fmt.Sprintf(`%3.1f`, bpm) +} + +func BPMToCV(bpm float32) units.CV { + return units.CV(europim.InverseLerp(bpm, MinBPM, MaxBPM)) +} + +func CVToBPM(cv units.CV) float32 { + return europim.LerpRound(cv.ToFloat32(), MinBPM, MaxBPM) +} diff --git a/internal/projects/clockgenerator/module/setting_gateduration.go b/internal/projects/clockgenerator/module/setting_gateduration.go new file mode 100644 index 0000000..2c5fd3e --- /dev/null +++ b/internal/projects/clockgenerator/module/setting_gateduration.go @@ -0,0 +1,25 @@ +package module + +import ( + "time" + + europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/units" +) + +const ( + MinGateDuration time.Duration = time.Microsecond + MaxGateDuration time.Duration = time.Millisecond * 990 +) + +func GateDurationString(dur time.Duration) string { + return units.DurationString(dur) +} + +func GateDurationToCV(dur time.Duration) units.CV { + return units.CV(europim.InverseLerp(dur, MinGateDuration, MaxGateDuration)) +} + +func CVToGateDuration(cv units.CV) time.Duration { + return europim.Lerp[time.Duration](cv.ToFloat32(), MinGateDuration, MaxGateDuration) +} diff --git a/internal/projects/clockgenerator/screen/main.go b/internal/projects/clockgenerator/screen/main.go new file mode 100644 index 0000000..d9cc00c --- /dev/null +++ b/internal/projects/clockgenerator/screen/main.go @@ -0,0 +1,40 @@ +package screen + +import ( + "fmt" + "machine" + "time" + + "github.com/heucuva/europi" + "github.com/heucuva/europi/internal/projects/clockgenerator/module" + "github.com/heucuva/europi/output" +) + +type Main struct { + Clock *module.ClockGenerator +} + +const ( + line1y int16 = 11 + line2y int16 = 23 +) + +func (m *Main) Start(e *europi.EuroPi) { +} + +func (m *Main) Button1Debounce() time.Duration { + return time.Millisecond * 200 +} + +func (m *Main) Button1(e *europi.EuroPi, p machine.Pin) { + m.Clock.Toggle() +} + +func (m *Main) Paint(e *europi.EuroPi, deltaTime time.Duration) { + disp := e.Display + if m.Clock.Enabled() { + disp.DrawHLine(0, 0, 7, output.White) + } + disp.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y) + disp.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y) +} diff --git a/internal/projects/clockgenerator/screen/settings.go b/internal/projects/clockgenerator/screen/settings.go new file mode 100644 index 0000000..e645fa3 --- /dev/null +++ b/internal/projects/clockgenerator/screen/settings.go @@ -0,0 +1,64 @@ +package screen + +import ( + "machine" + "time" + + "github.com/heucuva/europi" + "github.com/heucuva/europi/experimental/knobmenu" + "github.com/heucuva/europi/internal/projects/clockgenerator/module" + "github.com/heucuva/europi/units" +) + +type Settings struct { + km *knobmenu.KnobMenu + Clock *module.ClockGenerator +} + +func (m *Settings) bpmString() string { + return module.BPMString(m.Clock.BPM()) +} + +func (m *Settings) bpmValue() units.CV { + return module.BPMToCV(m.Clock.BPM()) +} + +func (m *Settings) setBPMValue(value units.CV) { + m.Clock.SetBPM(module.CVToBPM(value)) +} + +func (m *Settings) gateDurationString() string { + return module.GateDurationString(m.Clock.GateDuration()) +} + +func (m *Settings) gateDurationValue() units.CV { + return module.GateDurationToCV(m.Clock.GateDuration()) +} + +func (m *Settings) setGateDurationValue(value units.CV) { + m.Clock.SetGateDuration(module.CVToGateDuration(value)) +} + +func (m *Settings) Start(e *europi.EuroPi) { + km, err := knobmenu.NewKnobMenu(e.K1, + knobmenu.WithItem("bpm", "BPM", m.bpmString, m.bpmValue, m.setBPMValue), + knobmenu.WithItem("gateDuration", "Gate", m.gateDurationString, m.gateDurationValue, m.setGateDurationValue), + ) + if err != nil { + panic(err) + } + + m.km = km +} + +func (m *Settings) Button1Debounce() time.Duration { + return time.Millisecond * 200 +} + +func (m *Settings) Button1(e *europi.EuroPi, p machine.Pin) { + m.km.Next() +} + +func (m *Settings) Paint(e *europi.EuroPi, deltaTime time.Duration) { + m.km.Paint(e, deltaTime) +} diff --git a/internal/projects/randomskips/LICENSE.md b/internal/projects/randomskips/LICENSE.md new file mode 100644 index 0000000..b7aa0e2 --- /dev/null +++ b/internal/projects/randomskips/LICENSE.md @@ -0,0 +1,11 @@ +# Released under MIT License + +Copyright (c) 2023 Jason Crawford + +Portions Copyright (c) 2022 Adam Wonak + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/internal/projects/randomskips/README.md b/internal/projects/randomskips/README.md new file mode 100644 index 0000000..6407b84 --- /dev/null +++ b/internal/projects/randomskips/README.md @@ -0,0 +1,57 @@ +# Random Skips + +A random gate skipper based on YouTube video observations made about the operation of the Ladik S-090 module. + +## Scope of This App + +The scope of this app is to drive the CV 1 output as a gate output based on a percentage chance of 33%. When the input gate (or internal clock gate) goes high (CV >= 0.8V), then a random value is generated and compared against the chance that's provided - if the probability is sufficient enough, then the gate is let through for as long as it is still high on the input. The moment the gate goes low, the output also goes low and the detection process starts again. + +### Inputs + +- Digital Input = clock input (optional, see below) + +### Outputs + +- CV 1 = Random Gate output + +## Using Random Skips + +### Changing Screens + +Long-pressing (>=650ms) Button 2 on the EuroPi will transition to the next display in the chain. If you transition past the last item in the display chain, then the display will cycle to the first item. + +The order of the displays is: +- Main display +- Random Skips configuration +- Performance clock configuration + +#### Main Display + +The main display shows the voltages of the CV outputs on the EuroPi as well as the enabled status of the internal performance clock. + +While Random Skips is operating, you can toggle between using the external clock (default mode at startup) and the internal clock by pressing Button 1 on the EuroPi while on the main screen. When the internal clock mode is active, you will be informed by seeing a small bar ( `_` ) in the upper-left corner of the display. + +#### Random Skips Configuration + +By default, the settings of Random Skips are: +- Chance: 50.0% + +When on the Random Skips Configuration screen, pressing Button 1 on the EuroPi will cycle through the configuration items. The currently selected item for edit will be identified by an asterisk (`*`) character and it may be updated by turning Knob 1 of the EuroPi. Updates are applied immediately. + +#### Performance Clock Configuration + +By default, the settings of the Performance Clock are: +- Clock Rate: 120.0 BPM +- Gate Duration: 100.0 ms + +When on the Performance Clock Configuration screen, pressing Button 1 on the EuroPi will cycle through the configuration items. The currently selected item for edit will be identified by an asterisk (`*`) character and it may be updated by turning Knob 1 of the EuroPi. Updates are applied immediately. + +## Special Thanks + +- Adam Wonak +- Charlotte Cox +- Allen Synthesis +- Ladik.eu +- Mouser Electronics +- Waveshare Electronics +- Raspberry Pi Foundation diff --git a/internal/projects/randomskips/localtest/localtest.go b/internal/projects/randomskips/localtest/localtest.go new file mode 100644 index 0000000..0ae674d --- /dev/null +++ b/internal/projects/randomskips/localtest/localtest.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "math/rand" + "time" + + clockgenerator "github.com/heucuva/europi/internal/projects/clockgenerator/module" + "github.com/heucuva/europi/internal/projects/randomskips/module" + "github.com/heucuva/europi/units" +) + +var ( + skip module.RandomSkips + clock clockgenerator.ClockGenerator + + outMap map[string]float32 = make(map[string]float32) +) + +func reportVolts(name string, v float32) { + switch name { + //case "cv1": + case "cv2": + case "cv3": + case "cv4": + case "cv5": + case "cv6": + default: + old := outMap[name] + if old != v { + fmt.Printf("%s: %v Volts\n", name, v) + outMap[name] = v + } + } +} + +func panicVOct(name string) func(units.VOct) { + return func(voct units.VOct) { + v := voct.ToVolts() + reportVolts(name, v) + } +} + +func panicCV(name string) func(units.CV) { + return func(cv units.CV) { + v := cv.ToVolts() + reportVolts(name, v) + } +} + +func startLoop() { + setCV1 := panicCV("cv1") + + if err := skip.Init(module.Config{ + Gate: func(high bool) { // Gate 1 + if high { + setCV1(1.0) + } else { + setCV1(0.0) + } + }, + Chance: 2.0 / 3.0, + }); err != nil { + panic(err) + } + + if err := clock.Init(clockgenerator.Config{ + BPM: 120.0, + Enabled: true, + ClockOut: skip.Gate, + }); err != nil { + panic(err) + } +} + +func mainLoop(deltaTime time.Duration) { + clock.Tick(deltaTime) + skip.Tick(deltaTime) +} + +func main() { + startLoop() + + ticker := time.NewTicker(time.Millisecond * 1) + defer ticker.Stop() + prev := time.Now() + for { + now := <-ticker.C + mainLoop(now.Sub(prev)) + prev = now + } +} + +func init() { + rand.Seed(time.Now().Unix()) +} diff --git a/internal/projects/randomskips/module/config.go b/internal/projects/randomskips/module/config.go new file mode 100644 index 0000000..d862339 --- /dev/null +++ b/internal/projects/randomskips/module/config.go @@ -0,0 +1,6 @@ +package module + +type Config struct { + Gate func(high bool) + Chance float32 +} diff --git a/internal/projects/randomskips/module/module.go b/internal/projects/randomskips/module/module.go new file mode 100644 index 0000000..7e766f5 --- /dev/null +++ b/internal/projects/randomskips/module/module.go @@ -0,0 +1,65 @@ +package module + +import ( + "math/rand" + "time" + + "github.com/heucuva/europi/units" +) + +type RandomSkips struct { + gate func(high bool) + chance float32 + + active bool + lastInput bool + cv float32 + ac float32 // attenuated chance (cv * chance) +} + +func (m *RandomSkips) Init(config Config) error { + fnGate := config.Gate + if fnGate == nil { + fnGate = noopGate + } + m.gate = fnGate + m.chance = config.Chance + + m.SetCV(1) + return nil +} + +func noopGate(high bool) { +} + +func (m *RandomSkips) Gate(high bool) { + prev := m.active + lastInput := m.lastInput + next := prev + m.lastInput = high + + if high != lastInput && rand.Float32() < m.ac { + next = !prev + } + + if prev != next { + m.active = next + m.gate(next) + } +} + +func (m *RandomSkips) SetChance(chance float32) { + m.chance = chance +} + +func (m *RandomSkips) Chance() float32 { + return m.chance +} + +func (m *RandomSkips) SetCV(cv units.CV) { + m.cv = cv.ToFloat32() + m.ac = m.chance * m.cv +} + +func (m *RandomSkips) Tick(deltaTime time.Duration) { +} diff --git a/internal/projects/randomskips/module/setting_chance.go b/internal/projects/randomskips/module/setting_chance.go new file mode 100644 index 0000000..a386511 --- /dev/null +++ b/internal/projects/randomskips/module/setting_chance.go @@ -0,0 +1,20 @@ +package module + +import ( + "fmt" + + europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/units" +) + +func ChanceString(chance float32) string { + return fmt.Sprintf("%3.1f%%", chance*100.0) +} + +func ChanceToCV(chance float32) units.CV { + return units.CV(chance) +} + +func CVToChance(cv units.CV) float32 { + return europim.Clamp(cv.ToFloat32(), 0.0, 1.0) +} diff --git a/internal/projects/randomskips/randomskips.go b/internal/projects/randomskips/randomskips.go new file mode 100644 index 0000000..18c64d8 --- /dev/null +++ b/internal/projects/randomskips/randomskips.go @@ -0,0 +1,92 @@ +package main + +import ( + "machine" + "time" + + "github.com/heucuva/europi" + "github.com/heucuva/europi/experimental/screenbank" + clockgenerator "github.com/heucuva/europi/internal/projects/clockgenerator/module" + clockScreen "github.com/heucuva/europi/internal/projects/clockgenerator/screen" + "github.com/heucuva/europi/internal/projects/randomskips/module" + "github.com/heucuva/europi/internal/projects/randomskips/screen" + "github.com/heucuva/europi/output" +) + +var ( + skip module.RandomSkips + clock clockgenerator.ClockGenerator + + ui *screenbank.ScreenBank + screenMain = screen.Main{ + RandomSkips: &skip, + Clock: &clock, + } + screenClock = clockScreen.Settings{ + Clock: &clock, + } + screenSettings = screen.Settings{ + RandomSkips: &skip, + } +) + +func makeGate(out output.Output) func(high bool) { + return func(high bool) { + if high { + out.On() + } else { + out.Off() + } + } +} + +func startLoop(e *europi.EuroPi) { + if err := skip.Init(module.Config{ + Gate: makeGate(e.CV1), + Chance: 0.5, + }); err != nil { + panic(err) + } + + if err := clock.Init(clockgenerator.Config{ + BPM: 120.0, + Enabled: false, + ClockOut: skip.Gate, + }); err != nil { + panic(err) + } + + e.DI.HandlerEx(machine.PinRising|machine.PinFalling, func(p machine.Pin) { + high := e.DI.Value() + skip.Gate(high) + }) +} + +func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { + clock.Tick(deltaTime) + skip.Tick(deltaTime) +} + +func main() { + var err error + ui, err = screenbank.NewScreenBank( + screenbank.WithScreen("main", "\u2b50", &screenMain), + screenbank.WithScreen("settings", "\u2611", &screenSettings), + screenbank.WithScreen("clock", "\u23f0", &screenClock), + ) + if err != nil { + panic(err) + } + + // some options shown below are being explicitly set to their defaults + // only to showcase their existence. + europi.Bootstrap( + europi.EnableDisplayLogger(false), + europi.InitRandom(true), + europi.StartLoop(startLoop), + europi.MainLoop(mainLoop), + europi.MainLoopInterval(time.Millisecond*1), + europi.UI(ui), + europi.UIRefreshRate(time.Millisecond*50), + ) +} diff --git a/internal/projects/randomskips/screen/main.go b/internal/projects/randomskips/screen/main.go new file mode 100644 index 0000000..392c7f9 --- /dev/null +++ b/internal/projects/randomskips/screen/main.go @@ -0,0 +1,42 @@ +package screen + +import ( + "fmt" + "machine" + "time" + + "github.com/heucuva/europi" + clockgenerator "github.com/heucuva/europi/internal/projects/clockgenerator/module" + "github.com/heucuva/europi/internal/projects/randomskips/module" + "github.com/heucuva/europi/output" +) + +type Main struct { + RandomSkips *module.RandomSkips + Clock *clockgenerator.ClockGenerator +} + +const ( + line1y int16 = 11 + line2y int16 = 23 +) + +func (m *Main) Start(e *europi.EuroPi) { +} + +func (m *Main) Button1Debounce() time.Duration { + return time.Millisecond * 200 +} + +func (m *Main) Button1(e *europi.EuroPi, p machine.Pin) { + m.Clock.Toggle() +} + +func (m *Main) Paint(e *europi.EuroPi, deltaTime time.Duration) { + disp := e.Display + if m.Clock.Enabled() { + disp.DrawHLine(0, 0, 7, output.White) + } + disp.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y) + disp.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y) +} diff --git a/internal/projects/randomskips/screen/settings.go b/internal/projects/randomskips/screen/settings.go new file mode 100644 index 0000000..dfcd7d3 --- /dev/null +++ b/internal/projects/randomskips/screen/settings.go @@ -0,0 +1,51 @@ +package screen + +import ( + "machine" + "time" + + "github.com/heucuva/europi" + "github.com/heucuva/europi/experimental/knobmenu" + "github.com/heucuva/europi/internal/projects/randomskips/module" + "github.com/heucuva/europi/units" +) + +type Settings struct { + km *knobmenu.KnobMenu + RandomSkips *module.RandomSkips +} + +func (m *Settings) chanceString() string { + return module.ChanceString(m.RandomSkips.Chance()) +} + +func (m *Settings) chanceValue() units.CV { + return module.ChanceToCV(m.RandomSkips.Chance()) +} + +func (m *Settings) setChanceValue(value units.CV) { + m.RandomSkips.SetChance(module.CVToChance(value)) +} + +func (m *Settings) Start(e *europi.EuroPi) { + km, err := knobmenu.NewKnobMenu(e.K1, + knobmenu.WithItem("chance", "Chance", m.chanceString, m.chanceValue, m.setChanceValue), + ) + if err != nil { + panic(err) + } + + m.km = km +} + +func (m *Settings) Button1Debounce() time.Duration { + return time.Millisecond * 200 +} + +func (m *Settings) Button1(e *europi.EuroPi, p machine.Pin) { + m.km.Next() +} + +func (m *Settings) Paint(e *europi.EuroPi, deltaTime time.Duration) { + m.km.Paint(e, deltaTime) +} diff --git a/math/lerp.go b/math/lerp.go index f2a1e1f..3c572d6 100644 --- a/math/lerp.go +++ b/math/lerp.go @@ -1,5 +1,7 @@ package math +import "math" + type Lerpable interface { ~uint8 | ~uint16 | ~int | ~float32 | ~int32 | ~int64 } @@ -7,3 +9,15 @@ type Lerpable interface { func Lerp[V Lerpable](t float32, low, high V) V { return V(t*float32(high-low)) + low } + +func LerpRound[V Lerpable](t float32, low, high V) V { + l := math.Round(float64(t) * float64(high-low)) + return Clamp(V(l)+low, low, high) +} + +func InverseLerp[V Lerpable](v, low, high V) float32 { + if high == low { + return 0 + } + return float32(v-low) / float32(high-low) +} diff --git a/output/alignment.go b/output/alignment.go new file mode 100644 index 0000000..bac6b36 --- /dev/null +++ b/output/alignment.go @@ -0,0 +1,17 @@ +package output + +type HorizontalAlignment int + +const ( + AlignLeft = HorizontalAlignment(iota) + AlignCenter + AlignRight +) + +type VerticalAlignment int + +const ( + AlignTop = VerticalAlignment(iota) + AlignMiddle + AlignBottom +) diff --git a/output/display.go b/output/display.go index 7d3eeee..337320c 100644 --- a/output/display.go +++ b/output/display.go @@ -7,6 +7,7 @@ import ( "tinygo.org/x/drivers/ssd1306" "tinygo.org/x/tinydraw" "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/notoemoji" "tinygo.org/x/tinyfont/proggy" ) @@ -18,9 +19,11 @@ const ( ) var ( - DefaultChannel = machine.I2C0 - DefaultFont = &proggy.TinySZ8pt7b - White = color.RGBA{255, 255, 255, 255} + DefaultChannel = machine.I2C0 + DefaultFont = &proggy.TinySZ8pt7b + DefaultEmojiFont = ¬oemoji.NotoEmojiRegular12pt + White = color.RGBA{255, 255, 255, 255} + Black = color.RGBA{0, 0, 0, 255} ) // Display is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED. @@ -53,7 +56,92 @@ func (d *Display) SetFont(font *tinyfont.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) + d.WriteLineAligned(text, x, y, AlignLeft, AlignTop) +} + +// WriteEmojiLineAligned writes the given emoji text to the display where: +// x, y is the bottom leftmost pixel of the text +// alignh is horizontal alignment +// alignv is vertical alignment +func (d *Display) WriteEmojiLineAligned(text string, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { + d.writeLineAligned(text, d.font, x, y, alignh, alignv) +} + +// 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 (d *Display) WriteLineAligned(text string, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { + d.writeLineAligned(text, d.font, x, y, alignh, alignv) +} + +func (d *Display) writeLineAligned(text string, font tinyfont.Fonter, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { + x0, y0 := x, y + switch alignh { + case AlignLeft: + case AlignCenter: + _, outerWidth := tinyfont.LineWidth(font, text) + x0 = (OLEDWidth-int16(outerWidth))/2 - x + case AlignRight: + _, outerWidth := tinyfont.LineWidth(font, text) + x0 = OLEDWidth - int16(outerWidth) - x + default: + panic("invalid alignment") + } + tinyfont.WriteLine(d, font, x0, y0, text, White) +} + +// WriteLineInverse writes the given text to the display in an inverted way where x, y is the bottom leftmost pixel of the text +func (d *Display) WriteLineInverse(text string, x, y int16) { + d.WriteLineInverseAligned(text, x, y, AlignLeft, AlignTop) +} + +// 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 (d *Display) WriteLineInverseAligned(text string, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { + d.writeLineInverseAligned(text, d.font, x, y, alignh, alignv) +} + +// WriteEmojiLineInverseAligned writes the given emoji 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 (d *Display) WriteEmojiLineInverseAligned(text string, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { + d.writeLineInverseAligned(text, DefaultEmojiFont, x, y, alignh, alignv) +} + +func (d *Display) writeLineInverseAligned(text string, font tinyfont.Fonter, x, y int16, 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: + x0 = (OLEDWidth-int16(outerWidth))/2 - x + x1 = x0 + 1 + case AlignRight: + x0 = OLEDWidth - int16(outerWidth) - x + x1 = x0 + 1 + default: + panic("invalid alignment") + } + switch alignv { + case AlignTop: + case AlignMiddle: + midY := (OLEDHeight - outerHeight) / 2 + y0 += midY + y1 += midY + case AlignBottom: + y1 = OLEDHeight - y1 + y0 = y1 - outerHeight + 2 + default: + panic("invalid alignment") + } + tinydraw.FilledRectangle(d, x0, y0, int16(outerWidth+2), outerHeight, White) + tinyfont.WriteLine(d, font, x1, y1, text, Black) } // DrawHLine draws a horizontal line diff --git a/output/output.go b/output/output.go index cc7d231..6df9684 100644 --- a/output/output.go +++ b/output/output.go @@ -3,8 +3,12 @@ package output import ( "log" "machine" + "math" + "runtime/interrupt" + "runtime/volatile" europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/units" ) const ( @@ -26,6 +30,8 @@ var defaultPeriod uint64 = 500 type Output interface { Get() uint32 SetVoltage(v float32) + SetCV(cv units.CV) + SetVOct(voct units.VOct) Set(v bool) On() Off() @@ -37,7 +43,7 @@ type output struct { pwm PWM pin machine.Pin ch uint8 - v float32 + v uint32 } // NewOutput returns a new Output interface. @@ -61,7 +67,10 @@ func NewOutput(pin machine.Pin, pwm PWM) Output { // 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) + state := interrupt.Disable() + v := o.pwm.Get(o.ch) + interrupt.Restore(state) + return v } // Set updates the current voltage high (true) or low (false) @@ -79,23 +88,39 @@ func (o *output) SetVoltage(v float32) { invertedCv := (v / MaxVoltage) * float32(o.pwm.Top()) // cv := (float32(o.pwm.Top()) - invertedCv) - CalibratedOffset cv := float32(invertedCv) - CalibratedOffset + state := interrupt.Disable() o.pwm.Set(o.ch, uint32(cv)) - o.v = v + interrupt.Restore(state) + volatile.StoreUint32(&o.v, math.Float32bits(v)) +} + +// SetCV sets the current output voltage based on a CV value +func (o *output) SetCV(cv units.CV) { + o.SetVoltage(cv.ToVolts()) +} + +// SetCV sets the current output voltage based on a V/Octave value +func (o *output) SetVOct(voct units.VOct) { + o.SetVoltage(voct.ToVolts()) } // On sets the current voltage high at 10.0v. func (o *output) On() { - o.v = MaxVoltage + volatile.StoreUint32(&o.v, math.Float32bits(MaxVoltage)) + state := interrupt.Disable() o.pwm.Set(o.ch, o.pwm.Top()) + interrupt.Restore(state) } // Off sets the current voltage low at 0.0v. func (o *output) Off() { + volatile.StoreUint32(&o.v, math.Float32bits(MinVoltage)) + state := interrupt.Disable() o.pwm.Set(o.ch, 0) - o.v = MinVoltage + interrupt.Restore(state) } // Voltage returns the current voltage func (o *output) Voltage() float32 { - return o.v + return math.Float32frombits(volatile.LoadUint32(&o.v)) } diff --git a/units/bipolarcv.go b/units/bipolarcv.go new file mode 100644 index 0000000..f22fce3 --- /dev/null +++ b/units/bipolarcv.go @@ -0,0 +1,21 @@ +package units + +// 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 { + v := float32(c) + range_check(v, -1, 1, "bipolarcv") + return v * 5 +} + +// ToCV converts a (normalized) BipolarCV value to a (normalized) CV value +func (c BipolarCV) ToCV() CV { + return CV((c.ToFloat32() + 1.0) * 0.5) +} + +// ToFloat32 returns a (normalized) BipolarCV value to its floating point representation [-1.0 .. 1.0] +func (c BipolarCV) ToFloat32() float32 { + return float32(c) +} diff --git a/units/cv.go b/units/cv.go index e71901d..7a374da 100644 --- a/units/cv.go +++ b/units/cv.go @@ -10,6 +10,11 @@ func (c CV) ToVolts() float32 { return v * 5 } +// ToBipolarCV converts a (normalized) CV value to a (normalized) BipolarCV value +func (c CV) ToBipolarCV() BipolarCV { + return BipolarCV(c.ToFloat32()*2.0 - 1.0) +} + // ToFloat32 returns a (normalized) CV value to its floating point representation [0.0 .. 1.0] func (c CV) ToFloat32() float32 { return float32(c) diff --git a/units/duration.go b/units/duration.go new file mode 100644 index 0000000..aa567be --- /dev/null +++ b/units/duration.go @@ -0,0 +1,17 @@ +package units + +import ( + "fmt" + "time" +) + +func DurationString(dur time.Duration) string { + switch { + case dur < time.Millisecond: + return fmt.Sprintf("%3.1fus", dur.Seconds()*1000000.0) + case dur < time.Second: + return fmt.Sprintf("%3.1fms", dur.Seconds()*1000.0) + default: + return fmt.Sprint(dur) + } +} diff --git a/units/hertz.go b/units/hertz.go new file mode 100644 index 0000000..d9f9168 --- /dev/null +++ b/units/hertz.go @@ -0,0 +1,25 @@ +package units + +import ( + "fmt" + "time" +) + +type Hertz float32 + +func (h Hertz) ToPeriod() time.Duration { + return time.Duration(float32(time.Second) / float32(h)) +} + +func (h Hertz) String() string { + switch { + case h < 0.001: + return fmt.Sprintf("%3.1fuHz", h*1000000.0) + case h < 1: + return fmt.Sprintf("%3.1fmHz", h*1000.0) + case h >= 1000: + return fmt.Sprintf("%3.1fkHz", h/1000.0) + default: + return fmt.Sprintf("%5.1fHz", h) + } +} diff --git a/units/voct.go b/units/voct.go index a77a81e..d2bfc56 100644 --- a/units/voct.go +++ b/units/voct.go @@ -1,5 +1,10 @@ package units +const ( + MinVOct VOct = 0.0 + MaxVOct VOct = 10.0 +) + // VOct is a representation of a Volt-per-Octave value type VOct float32 From ae09d68837273372c425f731a0a0ab1411d5d039 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 20 Apr 2023 08:51:43 -0700 Subject: [PATCH 10/62] remove local testing junk --- .../clockgenerator/localtest/localtest.go | 93 ------------------ .../randomskips/localtest/localtest.go | 96 ------------------- 2 files changed, 189 deletions(-) delete mode 100644 internal/projects/clockgenerator/localtest/localtest.go delete mode 100644 internal/projects/randomskips/localtest/localtest.go diff --git a/internal/projects/clockgenerator/localtest/localtest.go b/internal/projects/clockgenerator/localtest/localtest.go deleted file mode 100644 index da580a1..0000000 --- a/internal/projects/clockgenerator/localtest/localtest.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "fmt" - "math/rand" - "time" - - "github.com/heucuva/europi/internal/projects/clockgenerator/module" - "github.com/heucuva/europi/units" -) - -var ( - clockgenerator module.ClockGenerator - - outMap map[string]float32 = make(map[string]float32) -) - -func reportVolts(name string, v float32) { - switch name { - //case "cv1": - case "cv2": - case "cv3": - case "cv4": - case "cv5": - case "cv6": - default: - old := outMap[name] - if old != v { - fmt.Printf("%s: %v Volts\n", name, v) - outMap[name] = v - } - } -} - -func panicVOct(name string) func(units.VOct) { - return func(voct units.VOct) { - v := voct.ToVolts() - reportVolts(name, v) - } -} - -func panicCV(name string) func(units.CV) { - return func(cv units.CV) { - v := cv.ToVolts() - reportVolts(name, v) - } -} - -func bipolarOut(out func(units.CV)) func(cv units.BipolarCV) { - return func(cv units.BipolarCV) { - out(cv.ToCV()) - } -} - -func startLoop() { - setCV1 := panicCV("cv1") - - if err := clockgenerator.Init(module.Config{ - ClockOut: func(high bool) { - if high { - setCV1(1) - } else { - setCV1(0) - } - }, - BPM: 120.0, - GateDuration: time.Millisecond * 100, - Enabled: true, - }); err != nil { - panic(err) - } -} - -func mainLoop(deltaTime time.Duration) { - clockgenerator.Tick(deltaTime) -} - -func main() { - startLoop() - - ticker := time.NewTicker(time.Millisecond * 1) - defer ticker.Stop() - prev := time.Now() - for { - now := <-ticker.C - mainLoop(now.Sub(prev)) - prev = now - } -} - -func init() { - rand.Seed(time.Now().Unix()) -} diff --git a/internal/projects/randomskips/localtest/localtest.go b/internal/projects/randomskips/localtest/localtest.go deleted file mode 100644 index 0ae674d..0000000 --- a/internal/projects/randomskips/localtest/localtest.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "fmt" - "math/rand" - "time" - - clockgenerator "github.com/heucuva/europi/internal/projects/clockgenerator/module" - "github.com/heucuva/europi/internal/projects/randomskips/module" - "github.com/heucuva/europi/units" -) - -var ( - skip module.RandomSkips - clock clockgenerator.ClockGenerator - - outMap map[string]float32 = make(map[string]float32) -) - -func reportVolts(name string, v float32) { - switch name { - //case "cv1": - case "cv2": - case "cv3": - case "cv4": - case "cv5": - case "cv6": - default: - old := outMap[name] - if old != v { - fmt.Printf("%s: %v Volts\n", name, v) - outMap[name] = v - } - } -} - -func panicVOct(name string) func(units.VOct) { - return func(voct units.VOct) { - v := voct.ToVolts() - reportVolts(name, v) - } -} - -func panicCV(name string) func(units.CV) { - return func(cv units.CV) { - v := cv.ToVolts() - reportVolts(name, v) - } -} - -func startLoop() { - setCV1 := panicCV("cv1") - - if err := skip.Init(module.Config{ - Gate: func(high bool) { // Gate 1 - if high { - setCV1(1.0) - } else { - setCV1(0.0) - } - }, - Chance: 2.0 / 3.0, - }); err != nil { - panic(err) - } - - if err := clock.Init(clockgenerator.Config{ - BPM: 120.0, - Enabled: true, - ClockOut: skip.Gate, - }); err != nil { - panic(err) - } -} - -func mainLoop(deltaTime time.Duration) { - clock.Tick(deltaTime) - skip.Tick(deltaTime) -} - -func main() { - startLoop() - - ticker := time.NewTicker(time.Millisecond * 1) - defer ticker.Stop() - prev := time.Now() - for { - now := <-ticker.C - mainLoop(now.Sub(prev)) - prev = now - } -} - -func init() { - rand.Seed(time.Now().Unix()) -} From 2f29a565bbe7a5466cbf40e4323a2a59ec94cecc Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 22 Apr 2023 18:33:30 -0700 Subject: [PATCH 11/62] Support for testability --- bootstrap.go | 6 +- bootstrap_features.go | 17 +- bootstrap_panic.go | 15 +- bootstrap_ui.go | 47 ++-- bootstrap_uimodule.go | 28 +-- bootstrapoptions.go | 1 + bootstrapoptions_features.go | 11 + clamp/clamp.go | 15 ++ debounce/debounce.go | 34 +++ europi.go | 76 +++--- experimental/displaylogger/logger.go | 76 ++++-- experimental/draw/colors.go | 8 + .../fontwriter}/alignment.go | 2 +- experimental/fontwriter/writer.go | 112 +++++++++ experimental/knobbank/knobbank.go | 98 ++------ experimental/knobbank/knobbankentry.go | 62 +++++ experimental/knobbank/knobbankoptions.go | 11 +- experimental/knobbank/knoboptions.go | 24 +- experimental/knobmenu/knobmenu.go | 29 ++- experimental/knobmenu/options.go | 8 + experimental/quantizer/quantizer_round.go | 8 +- experimental/quantizer/quantizer_trunc.go | 8 +- experimental/screenbank/screenbank.go | 43 ++-- input/analog.go | 82 ------ input/analogreader.go | 22 -- input/button.go | 82 ------ input/digital.go | 84 ------- input/digitalreader.go | 15 -- input/knob.go | 67 ----- internal/event/bus.go | 40 +++ internal/hardware/README.md | 3 + internal/hardware/hal.go | 26 ++ internal/hardware/hal/analoginput.go | 19 ++ internal/hardware/hal/buttoninput.go | 3 + internal/hardware/hal/changes.go | 13 + internal/hardware/hal/digitalinput.go | 17 ++ internal/hardware/hal/displayoutput.go | 10 + internal/hardware/hal/hal.go | 27 ++ internal/hardware/hal/knobinput.go | 3 + internal/hardware/hal/randomgenerator.go | 7 + internal/hardware/hal/voltageoutput.go | 22 ++ internal/hardware/platform.go | 23 ++ internal/hardware/rev1/analoginput.go | 94 +++++++ internal/hardware/rev1/digitalinput.go | 61 +++++ internal/hardware/rev1/displayoutput.go | 41 +++ internal/hardware/rev1/messages.go | 32 +++ internal/hardware/rev1/nonpico.go | 178 +++++++++++++ internal/hardware/rev1/pico.go | 233 ++++++++++++++++++ internal/hardware/rev1/platform.go | 57 +++++ internal/hardware/rev1/randomgenerator.go | 29 +++ internal/hardware/rev1/voltageoutput.go | 93 +++++++ internal/hardware/revision.go | 16 ++ .../projects/clockgenerator/clockgenerator.go | 8 +- .../projects/clockgenerator/module/module.go | 4 +- .../clockgenerator/module/setting_bpm.go | 10 +- .../module/setting_gateduration.go | 10 +- .../projects/clockgenerator/screen/main.go | 26 +- .../clockgenerator/screen/settings.go | 3 +- internal/projects/clockwerk/clockwerk.go | 8 + internal/projects/diagnostics/diagnostics.go | 45 ++-- .../projects/randomskips/module/module.go | 6 +- .../randomskips/module/setting_chance.go | 4 +- internal/projects/randomskips/randomskips.go | 18 +- internal/projects/randomskips/screen/main.go | 24 +- .../projects/randomskips/screen/settings.go | 3 +- lerp/lerp.go | 22 ++ lerp/lerp32.go | 45 ++++ lerp/lerp64.go | 45 ++++ math/lerp.go | 23 -- math/math.go | 25 -- output/display.go | 155 ------------ output/output.go | 126 ---------- output/pwm.go | 16 -- 73 files changed, 1763 insertions(+), 1001 deletions(-) create mode 100644 clamp/clamp.go create mode 100644 debounce/debounce.go create mode 100644 experimental/draw/colors.go rename {output => experimental/fontwriter}/alignment.go (91%) create mode 100644 experimental/fontwriter/writer.go create mode 100644 experimental/knobbank/knobbankentry.go delete mode 100644 input/analog.go delete mode 100644 input/analogreader.go delete mode 100644 input/button.go delete mode 100644 input/digital.go delete mode 100644 input/digitalreader.go delete mode 100644 input/knob.go create mode 100644 internal/event/bus.go create mode 100644 internal/hardware/README.md create mode 100644 internal/hardware/hal.go create mode 100644 internal/hardware/hal/analoginput.go create mode 100644 internal/hardware/hal/buttoninput.go create mode 100644 internal/hardware/hal/changes.go create mode 100644 internal/hardware/hal/digitalinput.go create mode 100644 internal/hardware/hal/displayoutput.go create mode 100644 internal/hardware/hal/hal.go create mode 100644 internal/hardware/hal/knobinput.go create mode 100644 internal/hardware/hal/randomgenerator.go create mode 100644 internal/hardware/hal/voltageoutput.go create mode 100644 internal/hardware/platform.go create mode 100644 internal/hardware/rev1/analoginput.go create mode 100644 internal/hardware/rev1/digitalinput.go create mode 100644 internal/hardware/rev1/displayoutput.go create mode 100644 internal/hardware/rev1/messages.go create mode 100644 internal/hardware/rev1/nonpico.go create mode 100644 internal/hardware/rev1/pico.go create mode 100644 internal/hardware/rev1/platform.go create mode 100644 internal/hardware/rev1/randomgenerator.go create mode 100644 internal/hardware/rev1/voltageoutput.go create mode 100644 internal/hardware/revision.go create mode 100644 lerp/lerp.go create mode 100644 lerp/lerp32.go create mode 100644 lerp/lerp64.go delete mode 100644 math/lerp.go delete mode 100644 math/math.go delete mode 100644 output/display.go delete mode 100644 output/output.go delete mode 100644 output/pwm.go diff --git a/bootstrap.go b/bootstrap.go index 4477e87..ce566a3 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -20,6 +20,7 @@ func Bootstrap(options ...BootstrapOption) error { panicHandler: DefaultPanicHandler, enableDisplayLogger: DefaultEnableDisplayLogger, initRandom: DefaultInitRandom, + europi: nil, onPostBootstrapConstructionFn: DefaultPostBootstrapInitialization, onPreInitializeComponentsFn: nil, @@ -38,7 +39,10 @@ func Bootstrap(options ...BootstrapOption) error { } } - e := New() + if config.europi == nil { + config.europi = New() + } + e := config.europi Pi = e piWantDestroyChan = make(chan struct{}, 1) diff --git a/bootstrap_features.go b/bootstrap_features.go index 125ecb8..ed3762c 100644 --- a/bootstrap_features.go +++ b/bootstrap_features.go @@ -2,15 +2,14 @@ package europi import ( "log" - "machine" - "math/rand" "os" "github.com/heucuva/europi/experimental/displaylogger" + "github.com/heucuva/europi/internal/hardware/hal" ) var ( - dispLog *displaylogger.Logger + dispLog displaylogger.Logger ) func enableDisplayLogger(e *EuroPi) { @@ -20,13 +19,12 @@ func enableDisplayLogger(e *EuroPi) { } log.SetFlags(0) - dispLog = &displaylogger.Logger{ - Display: e.Display, - } + dispLog = displaylogger.NewLogger(e.Display) log.SetOutput(dispLog) } func disableDisplayLogger(e *EuroPi) { + flushDisplayLogger(e) dispLog = nil log.SetOutput(os.Stdout) } @@ -38,10 +36,9 @@ func flushDisplayLogger(e *EuroPi) { } func initRandom(e *EuroPi) { - xl, _ := machine.GetRNG() - xh, _ := machine.GetRNG() - x := int64(xh)<<32 | int64(xl) - rand.Seed(x) + if e.RND != nil { + e.RND.Configure(hal.RandomGeneratorConfig{}) + } } func uninitRandom(e *EuroPi) { diff --git a/bootstrap_panic.go b/bootstrap_panic.go index b7d96ad..699cbf1 100644 --- a/bootstrap_panic.go +++ b/bootstrap_panic.go @@ -4,7 +4,8 @@ import ( "fmt" "log" - "github.com/heucuva/europi/output" + "github.com/heucuva/europi/experimental/draw" + "tinygo.org/x/tinydraw" ) // DefaultPanicHandler is the default handler for panics @@ -15,6 +16,7 @@ var DefaultPanicHandler func(e *EuroPi, reason any) func handlePanicOnScreenLog(e *EuroPi, reason any) { if e == nil { // can't do anything if it's not enabled + return } // force display-logging to enabled @@ -22,21 +24,26 @@ func handlePanicOnScreenLog(e *EuroPi, reason any) { // show the panic on the screen log.Panicln(fmt.Sprint(reason)) + + flushDisplayLogger(e) } func handlePanicDisplayCrash(e *EuroPi, reason any) { if e == nil { // can't do anything if it's not enabled + return } // display a diagonal line pattern through the screen to show that the EuroPi is crashed - ymax := int16(output.OLEDHeight) - 1 - for x := -ymax; x < output.OLEDWidth; x += 4 { + disp := e.Display + width, height := disp.Size() + ymax := height - 1 + for x := -ymax; x < width; x += 4 { lx, ly := x, int16(0) if x < 0 { lx = 0 ly = -x } - e.Display.DrawLine(lx, ly, x+ymax, ymax, output.White) + tinydraw.Line(e.Display, lx, ly, x+ymax, ymax, draw.White) } } diff --git a/bootstrap_ui.go b/bootstrap_ui.go index e972385..988438d 100644 --- a/bootstrap_ui.go +++ b/bootstrap_ui.go @@ -1,8 +1,9 @@ package europi import ( - "machine" "time" + + "github.com/heucuva/europi/debounce" ) type UserInterface interface { @@ -15,7 +16,7 @@ type UserInterfaceLogoPainter interface { } type UserInterfaceButton1 interface { - Button1(e *EuroPi, p machine.Pin) + Button1(e *EuroPi, deltaTime time.Duration) } type UserInterfaceButton1Debounce interface { @@ -23,15 +24,15 @@ type UserInterfaceButton1Debounce interface { } type UserInterfaceButton1Ex interface { - Button1Ex(e *EuroPi, p machine.Pin, high bool) + Button1Ex(e *EuroPi, value bool, deltaTime time.Duration) } type UserInterfaceButton1Long interface { - Button1Long(e *EuroPi, p machine.Pin) + Button1Long(e *EuroPi, deltaTime time.Duration) } type UserInterfaceButton2 interface { - Button2(e *EuroPi, p machine.Pin) + Button2(e *EuroPi, deltaTime time.Duration) } type UserInterfaceButton2Debounce interface { @@ -39,11 +40,11 @@ type UserInterfaceButton2Debounce interface { } type UserInterfaceButton2Ex interface { - Button2Ex(e *EuroPi, p machine.Pin, high bool) + Button2Ex(e *EuroPi, value bool, deltaTime time.Duration) } type UserInterfaceButton2Long interface { - Button2Long(e *EuroPi, p machine.Pin) + Button2Long(e *EuroPi, deltaTime time.Duration) } var ( @@ -61,21 +62,21 @@ func enableUI(e *EuroPi, screen UserInterface, interval time.Duration) { ui.repaint = make(chan struct{}, 1) var ( - inputB1 func(e *EuroPi, p machine.Pin, high bool) - inputB1L func(e *EuroPi, p machine.Pin) + inputB1 func(e *EuroPi, value bool, deltaTime time.Duration) + inputB1L func(e *EuroPi, deltaTime time.Duration) ) if in, ok := screen.(UserInterfaceButton1); ok { var debounceDelay time.Duration if db, ok := screen.(UserInterfaceButton1Debounce); ok { debounceDelay = db.Button1Debounce() } - var lastTrigger time.Time - inputB1 = func(e *EuroPi, p machine.Pin, high bool) { - now := time.Now() - if !high && (debounceDelay == 0 || now.Sub(lastTrigger) >= debounceDelay) { - lastTrigger = now - in.Button1(e, p) + inputDB := debounce.NewDebouncer(func(value bool, deltaTime time.Duration) { + if !value { + in.Button1(e, deltaTime) } + }).Debounce(debounceDelay) + inputB1 = func(e *EuroPi, value bool, deltaTime time.Duration) { + inputDB(value) } } else if in, ok := screen.(UserInterfaceButton1Ex); ok { inputB1 = in.Button1Ex @@ -86,21 +87,21 @@ func enableUI(e *EuroPi, screen UserInterface, interval time.Duration) { ui.setupButton(e, e.B1, inputB1, inputB1L) var ( - inputB2 func(e *EuroPi, p machine.Pin, high bool) - inputB2L func(e *EuroPi, p machine.Pin) + inputB2 func(e *EuroPi, value bool, deltaTime time.Duration) + inputB2L func(e *EuroPi, deltaTime time.Duration) ) if in, ok := screen.(UserInterfaceButton2); ok { var debounceDelay time.Duration if db, ok := screen.(UserInterfaceButton2Debounce); ok { debounceDelay = db.Button2Debounce() } - var lastTrigger time.Time - inputB2 = func(e *EuroPi, p machine.Pin, high bool) { - now := time.Now() - if !high && (debounceDelay == 0 || now.Sub(lastTrigger) >= debounceDelay) { - lastTrigger = now - in.Button2(e, p) + inputDB := debounce.NewDebouncer(func(value bool, deltaTime time.Duration) { + if !value { + in.Button2(e, deltaTime) } + }).Debounce(debounceDelay) + inputB2 = func(e *EuroPi, value bool, deltaTime time.Duration) { + inputDB(value) } } else if in, ok := screen.(UserInterfaceButton2Ex); ok { inputB2 = in.Button2Ex diff --git a/bootstrap_uimodule.go b/bootstrap_uimodule.go index 26773ca..48db79f 100644 --- a/bootstrap_uimodule.go +++ b/bootstrap_uimodule.go @@ -2,11 +2,10 @@ package europi import ( "context" - "machine" "sync" "time" - "github.com/heucuva/europi/input" + "github.com/heucuva/europi/internal/hardware/hal" ) type uiModule struct { @@ -59,37 +58,32 @@ func (u *uiModule) run(e *EuroPi, interval time.Duration) { } } -func (u *uiModule) setupButton(e *EuroPi, btn input.DigitalReader, onShort func(e *EuroPi, p machine.Pin, high bool), onLong func(e *EuroPi, p machine.Pin)) { +func (u *uiModule) setupButton(e *EuroPi, btn hal.ButtonInput, onShort func(e *EuroPi, value bool, deltaTime time.Duration), onLong func(e *EuroPi, deltaTime time.Duration)) { if onShort == nil && onLong == nil { return } if onShort == nil { // no-op - onShort = func(e *EuroPi, p machine.Pin, high bool) {} + onShort = func(e *EuroPi, value bool, deltaTime time.Duration) {} } // if no long-press handler present, just reuse short-press handler if onLong == nil { - onLong = func(e *EuroPi, p machine.Pin) { - onShort(e, p, false) + onLong = func(e *EuroPi, deltaTime time.Duration) { + onShort(e, false, deltaTime) } } const longDuration = time.Millisecond * 650 - btn.HandlerEx(machine.PinRising|machine.PinFalling, func(p machine.Pin) { - high := btn.Value() - if high { - onShort(e, p, high) + btn.HandlerEx(hal.ChangeAny, func(value bool, deltaTime time.Duration) { + if value { + onShort(e, value, deltaTime) + } else if deltaTime < longDuration { + onShort(e, value, deltaTime) } else { - startDown := btn.LastChange() - deltaTime := time.Since(startDown) - if deltaTime < longDuration { - onShort(e, p, high) - } else { - onLong(e, p) - } + onLong(e, deltaTime) } }) } diff --git a/bootstrapoptions.go b/bootstrapoptions.go index 2cc24dd..2ed1174 100644 --- a/bootstrapoptions.go +++ b/bootstrapoptions.go @@ -12,6 +12,7 @@ type bootstrapConfig struct { panicHandler func(e *EuroPi, reason any) enableDisplayLogger bool initRandom bool + europi *EuroPi // user interface ui UserInterface diff --git a/bootstrapoptions_features.go b/bootstrapoptions_features.go index 3f56abf..a76084b 100644 --- a/bootstrapoptions_features.go +++ b/bootstrapoptions_features.go @@ -49,3 +49,14 @@ func InitRandom(enabled bool) BootstrapOption { return nil } } + +func UsingEuroPi(e *EuroPi) BootstrapOption { + return func(o *bootstrapConfig) error { + if e == nil { + return errors.New("europi instance must not be nil") + } + + o.europi = e + return nil + } +} 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/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/europi.go b/europi.go index fc3057c..3945d39 100644 --- a/europi.go +++ b/europi.go @@ -1,55 +1,54 @@ package europi // import "github.com/heucuva/europi" import ( - "machine" - - "github.com/heucuva/europi/input" - "github.com/heucuva/europi/output" + "github.com/heucuva/europi/internal/hardware" + "github.com/heucuva/europi/internal/hardware/hal" ) // EuroPi is the collection of component wrappers used to interact with the module. type EuroPi struct { - // Display is a wrapper around ssd1306.Device - Display *output.Display - - DI input.DigitalReader - AI input.AnalogReader - - B1 input.DigitalReader - B2 input.DigitalReader - - K1 input.AnalogReader - K2 input.AnalogReader - - CV1 output.Output - CV2 output.Output - CV3 output.Output - CV4 output.Output - CV5 output.Output - CV6 output.Output - CV [6]output.Output + Display hal.DisplayOutput + DI hal.DigitalInput + AI hal.AnalogInput + B1 hal.ButtonInput + B2 hal.ButtonInput + K1 hal.KnobInput + K2 hal.KnobInput + CV1 hal.VoltageOutput + CV2 hal.VoltageOutput + CV3 hal.VoltageOutput + CV4 hal.VoltageOutput + CV5 hal.VoltageOutput + CV6 hal.VoltageOutput + CV [6]hal.VoltageOutput + RND hal.RandomGenerator } // New will return a new EuroPi struct. -func New() *EuroPi { - cv1 := output.NewOutput(machine.GPIO21, machine.PWM2) - cv2 := output.NewOutput(machine.GPIO20, machine.PWM2) - cv3 := output.NewOutput(machine.GPIO16, machine.PWM0) - cv4 := output.NewOutput(machine.GPIO17, machine.PWM0) - cv5 := output.NewOutput(machine.GPIO18, machine.PWM1) - cv6 := output.NewOutput(machine.GPIO19, machine.PWM1) +func New(opts ...hardware.Revision) *EuroPi { + revision := hardware.EuroPi + if len(opts) > 0 { + revision = opts[0] + } + + cv1 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage1Output) + cv2 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage2Output) + cv3 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage3Output) + cv4 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage4Output) + cv5 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage5Output) + cv6 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage6Output) e := &EuroPi{ - Display: output.NewDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1), + Display: hardware.GetHardware[hal.DisplayOutput](revision, hal.HardwareIdDisplay1Output), - DI: input.NewDigital(machine.GPIO22), - AI: input.NewAnalog(machine.ADC0), + DI: hardware.GetHardware[hal.DigitalInput](revision, hal.HardwareIdDigital1Input), + AI: hardware.GetHardware[hal.AnalogInput](revision, hal.HardwareIdAnalog1Input), - B1: input.NewButton(machine.GPIO4), - B2: input.NewButton(machine.GPIO5), + B1: hardware.GetHardware[hal.ButtonInput](revision, hal.HardwareIdButton1Input), + B2: hardware.GetHardware[hal.ButtonInput](revision, hal.HardwareIdButton2Input), - K1: input.NewKnob(machine.ADC1), - K2: input.NewKnob(machine.ADC2), + K1: hardware.GetHardware[hal.KnobInput](revision, hal.HardwareIdKnob1Input), + K2: hardware.GetHardware[hal.KnobInput](revision, hal.HardwareIdKnob2Input), CV1: cv1, CV2: cv2, @@ -57,7 +56,8 @@ func New() *EuroPi { CV4: cv4, CV5: cv5, CV6: cv5, - CV: [6]output.Output{cv1, cv2, cv3, cv4, cv5, cv6}, + CV: [6]hal.VoltageOutput{cv1, cv2, cv3, cv4, cv5, cv6}, + RND: hardware.GetHardware[hal.RandomGenerator](revision, hal.HardwareIdRandom1Generator), } return e diff --git a/experimental/displaylogger/logger.go b/experimental/displaylogger/logger.go index 6d277bc..43a642c 100644 --- a/experimental/displaylogger/logger.go +++ b/experimental/displaylogger/logger.go @@ -1,41 +1,42 @@ package displaylogger import ( + "io" "strings" - "github.com/heucuva/europi/output" + "github.com/heucuva/europi/experimental/draw" + "github.com/heucuva/europi/experimental/fontwriter" + "github.com/heucuva/europi/internal/hardware/hal" + "tinygo.org/x/tinyfont/proggy" ) -type Logger struct { - sb strings.Builder - Display *output.Display -} - -func (w *Logger) repaint() { - str := w.sb.String() +var ( + DefaultFont = &proggy.TinySZ8pt7b +) - fnt := output.DefaultFont - w.Display.SetFont(fnt) - w.Display.ClearBuffer() +type Logger interface { + io.Writer + Flush() +} - lines := strings.Split(str, "\n") - w.sb.Reset() - _, maxY := w.Display.Size() - maxLines := (maxY + int16(fnt.YAdvance) - 1) / int16(fnt.YAdvance) - for l := len(lines); l > int(maxLines); l-- { - lines = lines[1:] - } - w.sb.WriteString(strings.Join(lines, "\n")) +type logger struct { + sb strings.Builder + display hal.DisplayOutput + writer fontwriter.Writer +} - liney := fnt.YAdvance - for _, s := range lines { - w.Display.WriteLine(s, 0, int16(liney)) - liney += fnt.YAdvance +func NewLogger(display hal.DisplayOutput) Logger { + return &logger{ + sb: strings.Builder{}, + display: display, + writer: fontwriter.Writer{ + Display: display, + Font: DefaultFont, + }, } - _ = w.Display.Display() } -func (w *Logger) Write(p []byte) (n int, err error) { +func (w *logger) Write(p []byte) (n int, err error) { n, err = w.sb.Write(p) if err != nil { return @@ -45,6 +46,29 @@ func (w *Logger) Write(p []byte) (n int, err error) { return } -func (w *Logger) Flush() { +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..08859c5 --- /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{255, 255, 255, 255} +) diff --git a/output/alignment.go b/experimental/fontwriter/alignment.go similarity index 91% rename from output/alignment.go rename to experimental/fontwriter/alignment.go index bac6b36..b3ff377 100644 --- a/output/alignment.go +++ b/experimental/fontwriter/alignment.go @@ -1,4 +1,4 @@ -package output +package fontwriter type HorizontalAlignment int diff --git a/experimental/fontwriter/writer.go b/experimental/fontwriter/writer.go new file mode 100644 index 0000000..97b4331 --- /dev/null +++ b/experimental/fontwriter/writer.go @@ -0,0 +1,112 @@ +package fontwriter + +import ( + "image/color" + + "github.com/heucuva/europi/internal/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 index a527188..3ca1f21 100644 --- a/experimental/knobbank/knobbank.go +++ b/experimental/knobbank/knobbank.go @@ -1,20 +1,21 @@ package knobbank import ( - "github.com/heucuva/europi/input" - "github.com/heucuva/europi/math" - europim "github.com/heucuva/europi/math" + "errors" + + "github.com/heucuva/europi/clamp" + "github.com/heucuva/europi/internal/hardware/hal" "github.com/heucuva/europi/units" ) type KnobBank struct { - knob input.AnalogReader + knob hal.KnobInput current int lastValue float32 bank []knobBankEntry } -func NewKnobBank(knob input.AnalogReader, opts ...KnobBankOption) (*KnobBank, error) { +func NewKnobBank(knob hal.KnobInput, opts ...KnobBankOption) (*KnobBank, error) { kb := &KnobBank{ knob: knob, lastValue: knob.ReadVoltage(), @@ -29,6 +30,12 @@ func NewKnobBank(knob input.AnalogReader, opts ...KnobBankOption) (*KnobBank, er 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 "" @@ -40,12 +47,16 @@ func (kb *KnobBank) CurrentIndex() int { return kb.current } -func (kb *KnobBank) Current() input.AnalogReader { +func (kb *KnobBank) Current() hal.KnobInput { return kb } -func (kb *KnobBank) Samples(samples uint16) { - kb.knob.Samples(samples) +func (kb *KnobBank) MinVoltage() float32 { + return kb.MinVoltage() +} + +func (kb *KnobBank) MaxVoltage() float32 { + return kb.MaxVoltage() } func (kb *KnobBank) ReadVoltage() float32 { @@ -61,7 +72,7 @@ func (kb *KnobBank) ReadVoltage() float32 { } func (kb *KnobBank) ReadCV() units.CV { - return units.CV(math.Clamp(kb.Percent(), 0.0, 1.0)) + return units.CV(clamp.Clamp(kb.Percent(), 0.0, 1.0)) } func (kb *KnobBank) ReadVOct() units.VOct { @@ -80,23 +91,6 @@ func (kb *KnobBank) Percent() float32 { return cur.Percent() } -func (kb *KnobBank) Range(steps uint16) uint16 { - return kb.knob.Range(steps) -} - -func (kb *KnobBank) Choice(numItems int) int { - if len(kb.bank) == 0 { - return int(kb.Range(uint16(numItems))) - } - - cur := &kb.bank[kb.current] - value := kb.knob.ReadVoltage() - percent := kb.knob.Percent() - kb.lastValue = cur.update(percent, value, kb.lastValue) - idx := europim.Lerp(cur.Percent(), 0, 2*numItems+1) / 2 - return europim.Clamp(idx, 0, numItems-1) -} - func (kb *KnobBank) Next() { if len(kb.bank) == 0 { kb.current = 0 @@ -112,55 +106,3 @@ func (kb *KnobBank) Next() { } kb.bank[kb.current].unlock() } - -type knobBankEntry struct { - name string - enabled bool - locked bool - value float32 - percent float32 - minVoltage float32 - maxVoltage float32 - scale float32 -} - -func (e *knobBankEntry) lock(knob input.AnalogReader, 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 europim.Lerp[float32](e.percent*e.scale, 0, 1) -} - -func (e *knobBankEntry) Value() float32 { - return europim.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 europim.Abs(value-lastValue) < 0.05 { - return lastValue - } - - e.percent = percent - e.value = value - return value -} diff --git a/experimental/knobbank/knobbankentry.go b/experimental/knobbank/knobbankentry.go new file mode 100644 index 0000000..a2370f1 --- /dev/null +++ b/experimental/knobbank/knobbankentry.go @@ -0,0 +1,62 @@ +package knobbank + +import ( + "math" + + "github.com/heucuva/europi/clamp" + "github.com/heucuva/europi/internal/hardware/hal" + "github.com/heucuva/europi/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 index f9f831d..b9ee72b 100644 --- a/experimental/knobbank/knobbankoptions.go +++ b/experimental/knobbank/knobbankoptions.go @@ -2,8 +2,6 @@ package knobbank import ( "fmt" - - "github.com/heucuva/europi/input" ) type KnobBankOption func(kb *KnobBank) error @@ -15,14 +13,19 @@ func WithDisabledKnob() KnobBankOption { } } +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: input.MinVoltage, - maxVoltage: input.MaxVoltage, + minVoltage: defaultMinInputVoltage, + maxVoltage: defaultMaxInputVoltage, scale: 1, } diff --git a/experimental/knobbank/knoboptions.go b/experimental/knobbank/knoboptions.go index a6bf096..eead862 100644 --- a/experimental/knobbank/knoboptions.go +++ b/experimental/knobbank/knoboptions.go @@ -3,8 +3,7 @@ package knobbank import ( "fmt" - "github.com/heucuva/europi/input" - "github.com/heucuva/europi/math" + "github.com/heucuva/europi/lerp" ) type KnobOption func(e *knobBankEntry) error @@ -16,7 +15,26 @@ func InitialPercentageValue(v float32) KnobOption { } e.percent = v - e.value = math.Lerp[float32](v, input.MinVoltage, input.MaxVoltage) + 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/experimental/knobmenu/knobmenu.go b/experimental/knobmenu/knobmenu.go index 34fffc0..fa361db 100644 --- a/experimental/knobmenu/knobmenu.go +++ b/experimental/knobmenu/knobmenu.go @@ -5,9 +5,16 @@ import ( "time" "github.com/heucuva/europi" + "github.com/heucuva/europi/clamp" + "github.com/heucuva/europi/experimental/draw" + "github.com/heucuva/europi/experimental/fontwriter" "github.com/heucuva/europi/experimental/knobbank" - "github.com/heucuva/europi/input" - europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/internal/hardware/hal" + "tinygo.org/x/tinyfont/proggy" +) + +var ( + DefaultFont = &proggy.TinySZ8pt7b ) type KnobMenu struct { @@ -18,17 +25,25 @@ type KnobMenu struct { x int16 y int16 yadvance int16 + writer fontwriter.Writer } -func NewKnobMenu(knob input.AnalogReader, opts ...KnobMenuOption) (*KnobMenu, error) { +func NewKnobMenu(knob hal.KnobInput, opts ...KnobMenuOption) (*KnobMenu, error) { km := &KnobMenu{ selectedRune: '*', unselectedRune: ' ', x: 0, y: 11, yadvance: 12, + writer: fontwriter.Writer{ + Display: nil, + Font: DefaultFont, + }, } + km.yadvance = int16(km.writer.Font.GetYAdvance()) + km.y = km.yadvance + kbopts := []knobbank.KnobBankOption{ knobbank.WithDisabledKnob(), } @@ -59,12 +74,12 @@ func (m *KnobMenu) Next() { func (m *KnobMenu) Paint(e *europi.EuroPi, deltaTime time.Duration) { m.updateMenu(e) - disp := e.Display + m.writer.Display = e.Display y := m.y selectedIdx := m.kb.CurrentIndex() - 1 - minI := europim.Clamp(selectedIdx-1, 0, len(m.items)-1) - maxI := europim.Clamp(minI+1, 0, len(m.items)-1) + minI := clamp.Clamp(selectedIdx-1, 0, len(m.items)-1) + maxI := clamp.Clamp(minI+1, 0, len(m.items)-1) for i := minI; i <= maxI && i < len(m.items); i++ { it := &m.items[i] @@ -73,7 +88,7 @@ func (m *KnobMenu) Paint(e *europi.EuroPi, deltaTime time.Duration) { selRune = m.selectedRune } - disp.WriteLine(fmt.Sprintf("%c%s:%s", selRune, it.label, it.stringFn()), m.x, y) + m.writer.WriteLine(fmt.Sprintf("%c%s:%s", selRune, it.label, it.stringFn()), m.x, y, draw.White) y += m.yadvance } } diff --git a/experimental/knobmenu/options.go b/experimental/knobmenu/options.go index fad7237..679edbb 100644 --- a/experimental/knobmenu/options.go +++ b/experimental/knobmenu/options.go @@ -5,6 +5,7 @@ import ( "github.com/heucuva/europi/experimental/knobbank" "github.com/heucuva/europi/units" + "tinygo.org/x/tinyfont" ) type KnobMenuOption func(km *KnobMenu) ([]knobbank.KnobBankOption, error) @@ -44,3 +45,10 @@ func WithYAdvance(yadvance int16) KnobMenuOption { return nil, nil } } + +func WithFont(font tinyfont.Fonter) KnobMenuOption { + return func(km *KnobMenu) ([]knobbank.KnobBankOption, error) { + km.writer.Font = font + return nil, nil + } +} diff --git a/experimental/quantizer/quantizer_round.go b/experimental/quantizer/quantizer_round.go index 6e5b72a..1b83651 100644 --- a/experimental/quantizer/quantizer_round.go +++ b/experimental/quantizer/quantizer_round.go @@ -1,9 +1,7 @@ package quantizer import ( - "math" - - europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/lerp" ) type Round[T any] struct{} @@ -13,9 +11,7 @@ func (Round[T]) QuantizeToIndex(in float32, length int) int { return -1 } - idx := int(math.Round(float64(length-1) * float64(in))) - idx = europim.Clamp(idx, 0, length-1) - return idx + return lerp.NewLerp32(0, length-1).ClampedLerpRound(in) } func (q Round[T]) QuantizeToValue(in float32, list []T) T { diff --git a/experimental/quantizer/quantizer_trunc.go b/experimental/quantizer/quantizer_trunc.go index 4387e71..3759027 100644 --- a/experimental/quantizer/quantizer_trunc.go +++ b/experimental/quantizer/quantizer_trunc.go @@ -1,9 +1,7 @@ package quantizer import ( - "math" - - europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/lerp" ) type Trunc[T any] struct{} @@ -13,9 +11,7 @@ func (Trunc[T]) QuantizeToIndex(in float32, length int) int { return -1 } - idx := int(math.Trunc(float64(length-1) * float64(in))) - idx = europim.Clamp(idx, 0, length-1) - return idx + return lerp.NewLerp32(0, length-1).ClampedLerp(in) } func (q Trunc[T]) QuantizeToValue(in float32, list []T) T { diff --git a/experimental/screenbank/screenbank.go b/experimental/screenbank/screenbank.go index 35558b0..854238e 100644 --- a/experimental/screenbank/screenbank.go +++ b/experimental/screenbank/screenbank.go @@ -1,21 +1,31 @@ package screenbank import ( - "machine" "time" "github.com/heucuva/europi" - "github.com/heucuva/europi/output" + "github.com/heucuva/europi/experimental/draw" + "github.com/heucuva/europi/experimental/fontwriter" + "tinygo.org/x/tinyfont/notoemoji" ) type ScreenBank struct { screen europi.UserInterface current int bank []screenBankEntry + writer fontwriter.Writer } +var ( + DefaultFont = ¬oemoji.NotoEmojiRegular12pt +) + func NewScreenBank(opts ...ScreenBankOption) (*ScreenBank, error) { - sb := &ScreenBank{} + sb := &ScreenBank{ + writer: fontwriter.Writer{ + Font: DefaultFont, + }, + } for _, opt := range opts { if err := opt(sb); err != nil { @@ -90,7 +100,8 @@ func (sb *ScreenBank) PaintLogo(e *europi.EuroPi, deltaTime time.Duration) { cur := &sb.bank[sb.current] cur.lock() if cur.logo != "" { - e.Display.WriteEmojiLineInverseAligned(cur.logo, 0, 16, output.AlignRight, output.AlignMiddle) + sb.writer.Display = e.Display + sb.writer.WriteLineInverseAligned(cur.logo, 0, 16, draw.White, fontwriter.AlignRight, fontwriter.AlignMiddle) } cur.unlock() } @@ -108,37 +119,37 @@ func (sb *ScreenBank) Paint(e *europi.EuroPi, deltaTime time.Duration) { cur.unlock() } -func (sb *ScreenBank) Button1Ex(e *europi.EuroPi, p machine.Pin, high bool) { +func (sb *ScreenBank) Button1Ex(e *europi.EuroPi, value bool, deltaTime time.Duration) { screen := sb.Current() if cur, ok := screen.(europi.UserInterfaceButton1); ok { - if !high { - cur.Button1(e, p) + if !value { + cur.Button1(e, deltaTime) } } else if cur, ok := screen.(europi.UserInterfaceButton1Ex); ok { - cur.Button1Ex(e, p, high) + cur.Button1Ex(e, value, deltaTime) } } -func (sb *ScreenBank) Button1Long(e *europi.EuroPi, p machine.Pin) { +func (sb *ScreenBank) Button1Long(e *europi.EuroPi, deltaTime time.Duration) { if cur, ok := sb.Current().(europi.UserInterfaceButton1Long); ok { - cur.Button1Long(e, p) + cur.Button1Long(e, deltaTime) } else { // try the short-press - sb.Button1Ex(e, p, false) + sb.Button1Ex(e, false, deltaTime) } } -func (sb *ScreenBank) Button2Ex(e *europi.EuroPi, p machine.Pin, high bool) { +func (sb *ScreenBank) Button2Ex(e *europi.EuroPi, value bool, deltaTime time.Duration) { screen := sb.Current() if cur, ok := screen.(europi.UserInterfaceButton2); ok { - if !high { - cur.Button2(e, p) + if !value { + cur.Button2(e, deltaTime) } } else if cur, ok := screen.(europi.UserInterfaceButton2Ex); ok { - cur.Button2Ex(e, p, high) + cur.Button2Ex(e, value, deltaTime) } } -func (sb *ScreenBank) Button2Long(e *europi.EuroPi, p machine.Pin) { +func (sb *ScreenBank) Button2Long(e *europi.EuroPi, deltaTime time.Duration) { sb.Next() } diff --git a/input/analog.go b/input/analog.go deleted file mode 100644 index 9488cf0..0000000 --- a/input/analog.go +++ /dev/null @@ -1,82 +0,0 @@ -package input - -import ( - "machine" - "runtime/interrupt" - - europim "github.com/heucuva/europi/math" - "github.com/heucuva/europi/units" -) - -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 - - MaxVoltage = 10.0 - MinVoltage = 0.0 -) - -// 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 Analog struct { - machine.ADC - samples uint16 -} - -// NewAnalog creates a new Analog. -func NewAnalog(pin machine.Pin) *Analog { - adc := machine.ADC{Pin: pin} - adc.Configure(machine.ADCConfig{}) - return &Analog{ADC: adc, samples: DefaultSamples} -} - -// Samples sets the number of reads for an more accurate average read. -func (a *Analog) 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 *Analog) Percent() float32 { - return float32(a.read()) / CalibratedMaxAI -} - -// ReadVoltage return the current read voltage between 0.0 and 10.0 volts. -func (a *Analog) ReadVoltage() float32 { - return a.Percent() * MaxVoltage -} - -// ReadCV returns the current read voltage as a CV value. -func (a *Analog) ReadCV() units.CV { - // we can't use a.Percent() here, because we might get over 5.0 volts input - // just clamp it - v := a.ReadVoltage() - return units.CV(europim.Clamp(v/5.0, 0.0, 1.0)) -} - -// ReadCV returns the current read voltage as a V/Octave value. -func (a *Analog) ReadVOct() units.VOct { - return units.VOct(a.ReadVoltage()) -} - -// Range return a value between 0 and the given steps (not inclusive) based on the range of the analog input. -func (a *Analog) Range(steps uint16) uint16 { - return uint16(a.Percent() * float32(steps)) -} - -func (a *Analog) Choice(numItems int) int { - return europim.Lerp(a.Percent(), 0, numItems-1) -} - -func (a *Analog) read() uint16 { - var sum int - state := interrupt.Disable() - for i := 0; i < int(a.samples); i++ { - sum += europim.Clamp(int(a.Get())-CalibratedMinAI, 0, CalibratedMaxAI) - } - interrupt.Restore(state) - return uint16(sum / int(a.samples)) -} diff --git a/input/analogreader.go b/input/analogreader.go deleted file mode 100644 index cf0afea..0000000 --- a/input/analogreader.go +++ /dev/null @@ -1,22 +0,0 @@ -package input - -import ( - "machine" - - "github.com/heucuva/europi/units" -) - -// AnalogReader is an interface for common analog read methods for knobs and cv input. -type AnalogReader interface { - Samples(samples uint16) - ReadVoltage() float32 - ReadCV() units.CV - ReadVOct() units.VOct - Percent() float32 - Range(steps uint16) uint16 - Choice(numItems int) int -} - -func init() { - machine.InitADC() -} diff --git a/input/button.go b/input/button.go deleted file mode 100644 index c45ef64..0000000 --- a/input/button.go +++ /dev/null @@ -1,82 +0,0 @@ -package input - -import ( - "machine" - "runtime/interrupt" - "time" -) - -// Button is a struct for handling push button behavior. -type Button struct { - Pin machine.Pin - lastChange time.Time -} - -// NewButton creates a new Button struct. -func NewButton(pin machine.Pin) *Button { - pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) - return &Button{ - Pin: pin, - lastChange: time.Now(), - } -} - -// Handler sets the callback function to be call when the button is pressed. -func (b *Button) Handler(handler func(p machine.Pin)) { - if handler == nil { - panic("cannot set nil handler") - } - b.HandlerEx(machine.PinRising|machine.PinFalling, func(p machine.Pin) { - if b.Value() { - handler(p) - } - }) -} - -// HandlerEx sets the callback function to be call when the button changes in a specified way. -func (b *Button) HandlerEx(pinChange machine.PinChange, handler func(p machine.Pin)) { - if handler == nil { - panic("cannot set nil handler") - } - b.setHandler(pinChange, handler) -} - -// HandlerWithDebounce 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) { - if handler == nil { - panic("cannot set nil handler") - } - lastInput := time.Now() - b.Handler(func(p machine.Pin) { - now := time.Now() - if now.Before(lastInput.Add(delay)) { - return - } - handler(p) - lastInput = now - }) -} - -func (b *Button) setHandler(pinChange machine.PinChange, handler func(p machine.Pin)) { - state := interrupt.Disable() - b.Pin.SetInterrupt(pinChange, func(p machine.Pin) { - now := time.Now() - handler(p) - b.lastChange = now - }) - interrupt.Restore(state) -} - -// LastChange return the time of the last button input change. -func (b *Button) LastChange() time.Time { - return b.lastChange -} - -// Value returns true if button is currently pressed, else false. -func (b *Button) Value() bool { - state := interrupt.Disable() - // Invert signal to match expected behavior. - v := !b.Pin.Get() - interrupt.Restore(state) - return v -} diff --git a/input/digital.go b/input/digital.go deleted file mode 100644 index 7df136d..0000000 --- a/input/digital.go +++ /dev/null @@ -1,84 +0,0 @@ -package input - -import ( - "machine" - "runtime/interrupt" - "time" -) - -const DefaultDebounceDelay = time.Duration(50 * time.Millisecond) - -// Digital is a struct for handling reading of the digital input. -type Digital struct { - Pin machine.Pin - lastChange time.Time -} - -// NewDigital creates a new Digital struct. -func NewDigital(pin machine.Pin) *Digital { - pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) - return &Digital{ - Pin: pin, - lastChange: time.Now(), - } -} - -// LastChange return the time of the last input change (triggered at 0.8v). -func (d *Digital) LastChange() time.Time { - return d.lastChange -} - -// Value returns true if the input is high (above 0.8v), else false. -func (d *Digital) Value() bool { - state := interrupt.Disable() - // Invert signal to match expected behavior. - v := !d.Pin.Get() - interrupt.Restore(state) - return v -} - -// Handler sets the callback function to be call when the falling edge is detected. -func (d *Digital) Handler(handler func(p machine.Pin)) { - if handler == nil { - panic("cannot set nil handler") - } - d.HandlerEx(machine.PinRising|machine.PinFalling, func(p machine.Pin) { - if d.Value() { - handler(p) - } - }) -} - -// HandlerEx sets the callback function to be call when the input changes in a specified way. -func (d *Digital) HandlerEx(pinChange machine.PinChange, handler func(p machine.Pin)) { - if handler == nil { - panic("cannot set nil handler") - } - d.setHandler(pinChange, handler) -} - -// HandlerWithDebounce sets the callback function to be call when the falling edge is detected and debounce delay time has elapsed. -func (d *Digital) HandlerWithDebounce(handler func(p machine.Pin), delay time.Duration) { - if handler == nil { - panic("cannot set nil handler") - } - lastInput := time.Now() - d.Handler(func(p machine.Pin) { - now := time.Now() - if now.Before(lastInput.Add(delay)) { - return - } - handler(p) - lastInput = now - }) -} - -func (d *Digital) setHandler(pinChange machine.PinChange, handler func(p machine.Pin)) { - state := interrupt.Disable() - d.Pin.SetInterrupt(pinChange, func(p machine.Pin) { - now := time.Now() - handler(p) - d.lastChange = now - }) - interrupt.Restore(state) -} diff --git a/input/digitalreader.go b/input/digitalreader.go deleted file mode 100644 index 04807d1..0000000 --- a/input/digitalreader.go +++ /dev/null @@ -1,15 +0,0 @@ -package input - -import ( - "machine" - "time" -) - -// DigitalReader is an interface for common digital inputs methods. -type DigitalReader interface { - Handler(func(machine.Pin)) - HandlerEx(machine.PinChange, func(machine.Pin)) - HandlerWithDebounce(func(machine.Pin), time.Duration) - LastChange() time.Time - Value() bool -} diff --git a/input/knob.go b/input/knob.go deleted file mode 100644 index 1706a7a..0000000 --- a/input/knob.go +++ /dev/null @@ -1,67 +0,0 @@ -package input - -import ( - "machine" - "math" - "runtime/interrupt" - - europim "github.com/heucuva/europi/math" - "github.com/heucuva/europi/units" -) - -// 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 -} - -// ReadCV returns the current read voltage as a CV value. -func (k *Knob) ReadCV() units.CV { - return units.CV(k.Percent()) -} - -// ReadCV returns the current read voltage as a V/Octave value. -func (k *Knob) ReadVOct() units.VOct { - return units.VOct(k.ReadVoltage()) -} - -// 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) Choice(numItems int) int { - return europim.Lerp(k.Percent(), 0, numItems-1) -} - -func (k *Knob) read() uint16 { - var sum int - state := interrupt.Disable() - for i := 0; i < int(k.samples); i++ { - sum += int(k.Get()) - } - interrupt.Restore(state) - return uint16(sum / int(k.samples)) -} diff --git a/internal/event/bus.go b/internal/event/bus.go new file mode 100644 index 0000000..9bc5b37 --- /dev/null +++ b/internal/event/bus.go @@ -0,0 +1,40 @@ +package event + +import "sync" + +type Bus interface { + Subscribe(subject string, callback func(msg any)) + 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) 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/internal/hardware/README.md b/internal/hardware/README.md new file mode 100644 index 0000000..5059dc2 --- /dev/null +++ b/internal/hardware/README.md @@ -0,0 +1,3 @@ +# hardware + +This package is used for the [Original EuroPi hardware](https://github.com/Allen-Synthesis/EuroPi/tree/main/hardware). diff --git a/internal/hardware/hal.go b/internal/hardware/hal.go new file mode 100644 index 0000000..7f3ce76 --- /dev/null +++ b/internal/hardware/hal.go @@ -0,0 +1,26 @@ +package hardware + +type HardwareId int + +const ( + HardwareIdInvalid = HardwareId(iota) + HardwareIdDigital1Input + HardwareIdAnalog1Input + HardwareIdDisplay1Output + HardwareIdButton1Input + HardwareIdButton2Input + HardwareIdKnob1Input + HardwareIdKnob2Input + HardwareIdVoltage1Output + HardwareIdVoltage2Output + HardwareIdVoltage3Output + HardwareIdVoltage4Output + HardwareIdVoltage5Output + HardwareIdVoltage6Output + // NOTE: always ONLY append to this list, NEVER remove, rename, or reorder +) + +// aliases for friendly internationali(s|z)ation +const ( + HardwareIdAnalogue1Input = HardwareIdAnalog1Input +) diff --git a/internal/hardware/hal/analoginput.go b/internal/hardware/hal/analoginput.go new file mode 100644 index 0000000..94022a7 --- /dev/null +++ b/internal/hardware/hal/analoginput.go @@ -0,0 +1,19 @@ +package hal + +import "github.com/heucuva/europi/units" + +type AnalogInput interface { + Configure(config AnalogInputConfig) error + Percent() float32 + ReadVoltage() float32 + ReadCV() units.CV + ReadVOct() units.VOct + MinVoltage() float32 + MaxVoltage() float32 +} + +type AnalogInputConfig struct { + Samples int + CalibratedMinAI uint16 + CalibratedMaxAI uint16 +} diff --git a/internal/hardware/hal/buttoninput.go b/internal/hardware/hal/buttoninput.go new file mode 100644 index 0000000..ca67a05 --- /dev/null +++ b/internal/hardware/hal/buttoninput.go @@ -0,0 +1,3 @@ +package hal + +type ButtonInput = DigitalInput diff --git a/internal/hardware/hal/changes.go b/internal/hardware/hal/changes.go new file mode 100644 index 0000000..8c300c5 --- /dev/null +++ b/internal/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/internal/hardware/hal/digitalinput.go b/internal/hardware/hal/digitalinput.go new file mode 100644 index 0000000..dd208c0 --- /dev/null +++ b/internal/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/internal/hardware/hal/displayoutput.go b/internal/hardware/hal/displayoutput.go new file mode 100644 index 0000000..0f4a942 --- /dev/null +++ b/internal/hardware/hal/displayoutput.go @@ -0,0 +1,10 @@ +package hal + +import "image/color" + +type DisplayOutput interface { + ClearBuffer() + Size() (x, y int16) + SetPixel(x, y int16, c color.RGBA) + Display() error +} diff --git a/internal/hardware/hal/hal.go b/internal/hardware/hal/hal.go new file mode 100644 index 0000000..b0bebff --- /dev/null +++ b/internal/hardware/hal/hal.go @@ -0,0 +1,27 @@ +package hal + +type HardwareId int + +const ( + HardwareIdInvalid = HardwareId(iota) + HardwareIdDigital1Input + HardwareIdAnalog1Input + HardwareIdDisplay1Output + HardwareIdButton1Input + HardwareIdButton2Input + HardwareIdKnob1Input + HardwareIdKnob2Input + HardwareIdVoltage1Output + HardwareIdVoltage2Output + HardwareIdVoltage3Output + HardwareIdVoltage4Output + HardwareIdVoltage5Output + HardwareIdVoltage6Output + HardwareIdRandom1Generator + // NOTE: always ONLY append to this list, NEVER remove, rename, or reorder +) + +// aliases for friendly internationali(s|z)ation +const ( + HardwareIdAnalogue1Input = HardwareIdAnalog1Input +) diff --git a/internal/hardware/hal/knobinput.go b/internal/hardware/hal/knobinput.go new file mode 100644 index 0000000..888a8ec --- /dev/null +++ b/internal/hardware/hal/knobinput.go @@ -0,0 +1,3 @@ +package hal + +type KnobInput = AnalogInput diff --git a/internal/hardware/hal/randomgenerator.go b/internal/hardware/hal/randomgenerator.go new file mode 100644 index 0000000..93a194e --- /dev/null +++ b/internal/hardware/hal/randomgenerator.go @@ -0,0 +1,7 @@ +package hal + +type RandomGenerator interface { + Configure(config RandomGeneratorConfig) error +} + +type RandomGeneratorConfig struct{} diff --git a/internal/hardware/hal/voltageoutput.go b/internal/hardware/hal/voltageoutput.go new file mode 100644 index 0000000..7029137 --- /dev/null +++ b/internal/hardware/hal/voltageoutput.go @@ -0,0 +1,22 @@ +package hal + +import ( + "time" + + "github.com/heucuva/europi/units" +) + +type VoltageOutput interface { + SetVoltage(v float32) + SetCV(cv units.CV) + SetVOct(voct units.VOct) + Voltage() float32 + MinVoltage() float32 + MaxVoltage() float32 +} + +type VoltageOutputConfig struct { + Period time.Duration + Offset uint16 + Top uint16 +} diff --git a/internal/hardware/platform.go b/internal/hardware/platform.go new file mode 100644 index 0000000..8dd00b8 --- /dev/null +++ b/internal/hardware/platform.go @@ -0,0 +1,23 @@ +package hardware + +import ( + "github.com/heucuva/europi/internal/hardware/hal" + "github.com/heucuva/europi/internal/hardware/rev1" +) + +func GetHardware[T any](revision Revision, hw hal.HardwareId) T { + switch revision { + case Revision1: + hw, _ := rev1.GetHardware(hw).(T) + return hw + + case Revision2: + // TODO: implement hardware design of rev2 + hw, _ := rev1.GetHardware(hw).(T) + return hw + + default: + var none T + return none + } +} diff --git a/internal/hardware/rev1/analoginput.go b/internal/hardware/rev1/analoginput.go new file mode 100644 index 0000000..dd429f4 --- /dev/null +++ b/internal/hardware/rev1/analoginput.go @@ -0,0 +1,94 @@ +package rev1 + +import ( + "errors" + + "github.com/heucuva/europi/clamp" + "github.com/heucuva/europi/internal/hardware/hal" + "github.com/heucuva/europi/lerp" + "github.com/heucuva/europi/units" +) + +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 +) + +// 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.Lerper32[uint16] +} + +type adcProvider interface { + Get(samples int) uint16 +} + +// newAnalogInput creates a new Analog Input +func newAnalogInput(adc adcProvider) *analoginput { + return &analoginput{ + adc: adc, + samples: DefaultSamples, + cal: lerp.NewLerp32[uint16](DefaultCalibratedMinAI, DefaultCalibratedMaxAI), + } +} + +func (a *analoginput) Configure(config hal.AnalogInputConfig) error { + if config.Samples == 0 { + return errors.New("samples must be non-zero") + } + + if config.CalibratedMinAI == config.CalibratedMaxAI { + return errors.New("calibratedminai and calibratedmaxai must be different") + } else if config.CalibratedMinAI > config.CalibratedMaxAI { + return errors.New("calibtatedminai must be less than calibratedmaxai") + } + + a.samples = config.Samples + a.cal = lerp.NewLerp32(config.CalibratedMinAI, config.CalibratedMaxAI) + + return nil +} + +// ReadVoltage returns the current percentage read between 0.0 and 1.0. +func (a *analoginput) Percent() float32 { + return a.cal.InverseLerp(a.adc.Get(a.samples)) +} + +// ReadVoltage returns the current read voltage between 0.0 and 10.0 volts. +func (a *analoginput) ReadVoltage() float32 { + // NOTE: if MinInputVoltage ever becomes non-zero, then we need to use a lerp instead + return a.Percent() * MaxInputVoltage +} + +// ReadCV returns the current read voltage as a CV value. +func (a *analoginput) ReadCV() units.CV { + // we can't use a.Percent() here, because we might get over 5.0 volts input + // just clamp it + v := a.ReadVoltage() + return clamp.Clamp(units.CV(v/5.0), 0.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 +func (a *analoginput) MinVoltage() float32 { + return MinInputVoltage +} + +// MaxVoltage returns the maximum voltage that the input can ever read +func (a *analoginput) MaxVoltage() float32 { + return MaxInputVoltage +} diff --git a/internal/hardware/rev1/digitalinput.go b/internal/hardware/rev1/digitalinput.go new file mode 100644 index 0000000..9923184 --- /dev/null +++ b/internal/hardware/rev1/digitalinput.go @@ -0,0 +1,61 @@ +package rev1 + +import ( + "time" + + "github.com/heucuva/europi/debounce" + "github.com/heucuva/europi/internal/hardware/hal" +) + +// digitalinput is a struct for handling reading of the digital input. +type digitalinput struct { + dr digitalReaderProvider + lastChange time.Time +} + +type digitalReaderProvider interface { + Get() bool + SetHandler(changes hal.ChangeFlags, handler func()) +} + +// newDigitalInput creates a new digital input struct. +func newDigitalInput(dr digitalReaderProvider) *digitalinput { + return &digitalinput{ + dr: dr, + lastChange: time.Now(), + } +} + +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/internal/hardware/rev1/displayoutput.go b/internal/hardware/rev1/displayoutput.go new file mode 100644 index 0000000..188286b --- /dev/null +++ b/internal/hardware/rev1/displayoutput.go @@ -0,0 +1,41 @@ +package rev1 + +import ( + "image/color" + + "github.com/heucuva/europi/internal/hardware/hal" +) + +// displayoutput is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED. +type displayoutput struct { + dp displayProvider +} + +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) hal.DisplayOutput { + return &displayoutput{ + dp: dp, + } +} + +func (d *displayoutput) ClearBuffer() { + d.dp.ClearBuffer() +} + +func (d *displayoutput) Size() (x, y int16) { + return d.dp.Size() +} +func (d *displayoutput) SetPixel(x, y int16, c color.RGBA) { + d.dp.SetPixel(x, y, c) +} + +func (d *displayoutput) Display() error { + return d.dp.Display() +} diff --git a/internal/hardware/rev1/messages.go b/internal/hardware/rev1/messages.go new file mode 100644 index 0000000..ede1024 --- /dev/null +++ b/internal/hardware/rev1/messages.go @@ -0,0 +1,32 @@ +package rev1 + +import "github.com/heucuva/europi/internal/hardware/hal" + +type HwMessageDigitalValue struct { + Value bool +} + +type HwMessageADCValue struct { + Value uint16 +} + +type HwMessageInterrupt struct { + Change hal.ChangeFlags +} + +type HwMessagePwmValue struct { + Value uint16 +} + +type HwMessageDisplay struct { + Op HwDisplayOp + Operands []int16 +} + +type HwDisplayOp int + +const ( + HwDisplayOpClearBuffer = HwDisplayOp(iota) + HwDisplayOpSetPixel + HwDisplayOpDisplay +) diff --git a/internal/hardware/rev1/nonpico.go b/internal/hardware/rev1/nonpico.go new file mode 100644 index 0000000..4b0b7f7 --- /dev/null +++ b/internal/hardware/rev1/nonpico.go @@ -0,0 +1,178 @@ +//go:build !pico && test +// +build !pico,test + +package rev1 + +import ( + "fmt" + "image/color" + "math" + + "github.com/heucuva/europi/internal/event" + "github.com/heucuva/europi/internal/hardware/hal" +) + +var ( + DefaultEventBus = event.NewBus() +) + +//============= ADC =============// + +type nonPicoAdc struct { + bus event.Bus + id hal.HardwareId + value uint16 +} + +func newNonPicoAdc(bus event.Bus, id hal.HardwareId) adcProvider { + adc := &nonPicoAdc{ + bus: bus, + 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) +} + +//============= DigitalReader =============// + +type nonPicoDigitalReader struct { + bus event.Bus + id hal.HardwareId + value bool + handler func() +} + +func newNonPicoDigitalReader(bus event.Bus, id hal.HardwareId) digitalReaderProvider { + dr := &nonPicoDigitalReader{ + bus: bus, + id: id, + } + 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(d.bus, fmt.Sprintf("hw_interrupt_%d", d.id), func(msg HwMessageInterrupt) { + if (msg.Change & changes) != 0 { + handler() + } + }) +} + +//============= PWM =============// + +type nonPicoPwm struct { + bus event.Bus + id hal.HardwareId + v float32 +} + +func newNonPicoPwm(bus event.Bus, id hal.HardwareId) pwmProvider { + p := &nonPicoPwm{ + bus: bus, + id: id, + } + return p +} + +func (p *nonPicoPwm) Configure(config hal.VoltageOutputConfig) error { + return nil +} + +func (p *nonPicoPwm) Set(v float32, ofs uint16) { + invertedV := v * math.MaxUint16 + // volts := (float32(o.pwm.Top()) - invertedCv) - o.ofs + volts := invertedV - float32(ofs) + p.v = v + p.bus.Post(fmt.Sprintf("hw_pwm_%d", p.id), HwMessagePwmValue{ + Value: uint16(volts), + }) +} + +func (p *nonPicoPwm) Get() float32 { + return p.v +} + +//============= Display =============// + +const ( + oledWidth = 128 + oledHeight = 32 +) + +type nonPicoDisplayOutput struct { + bus event.Bus + id hal.HardwareId + width int16 + height int16 +} + +func newNonPicoDisplayOutput(bus event.Bus, id hal.HardwareId) displayProvider { + dp := &nonPicoDisplayOutput{ + bus: bus, + id: id, + width: oledWidth, + height: oledHeight, + } + + return dp +} + +func (d *nonPicoDisplayOutput) ClearBuffer() { + d.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) { + d.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 { + d.bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{ + Op: HwDisplayOpDisplay, + }) + return nil +} + +//============= Init =============// + +func init() { + hwDigital1Input = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdDigital1Input)) + hwAnalog1Input = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdAnalog1Input)) + hwDisplay1Output = newDisplayOutput(newNonPicoDisplayOutput(DefaultEventBus, hal.HardwareIdDisplay1Output)) + hwButton1Input = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdButton1Input)) + hwButton2Input = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdButton2Input)) + hwKnob1Input = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdKnob1Input)) + hwKnob2Input = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdKnob2Input)) + hwCV1Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage1Output)) + hwCV2Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage2Output)) + hwCV3Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage3Output)) + hwCV4Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage4Output)) + hwCV5Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage5Output)) + hwCV6Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage6Output)) + hwRandom1Generator = newRandomGenerator(nil) +} diff --git a/internal/hardware/rev1/pico.go b/internal/hardware/rev1/pico.go new file mode 100644 index 0000000..d8515b2 --- /dev/null +++ b/internal/hardware/rev1/pico.go @@ -0,0 +1,233 @@ +//go:build pico +// +build pico + +package rev1 + +import ( + "fmt" + "image/color" + "machine" + "math" + "math/rand" + "runtime/interrupt" + "runtime/volatile" + + "github.com/heucuva/europi/internal/hardware/hal" + "tinygo.org/x/drivers/ssd1306" +) + +//============= ADC =============// + +type picoAdc struct { + adc machine.ADC +} + +func newPicoAdc(pin machine.Pin) adcProvider { + 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) +} + +//============= DigitalReader =============// + +type picoDigitalReader struct { + pin machine.Pin +} + +func newPicoDigitalReader(pin machine.Pin) digitalReaderProvider { + 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 +} + +//============= PWM =============// + +type picoPwm struct { + pwm pwmGroup + pin machine.Pin + ch uint8 + v uint32 +} + +// 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 +} + +func newPicoPwm(pwm pwmGroup, pin machine.Pin) pwmProvider { + p := &picoPwm{ + pwm: pwm, + pin: pin, + } + return p +} + +func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error { + state := interrupt.Disable() + defer interrupt.Restore(state) + + err := p.pwm.Configure(machine.PWMConfig{ + Period: uint64(config.Period.Nanoseconds()), + }) + if err != nil { + return fmt.Errorf("pwm Configure error: %w", err) + } + + p.pwm.SetTop(uint32(config.Top)) + ch, err := p.pwm.Channel(p.pin) + if err != nil { + return fmt.Errorf("pwm Channel error: %w", err) + } + p.ch = ch + + return nil +} + +func (p *picoPwm) Set(v float32, ofs uint16) { + invertedV := v * float32(p.pwm.Top()) + // volts := (float32(o.pwm.Top()) - invertedCv) - o.ofs + volts := invertedV - float32(ofs) + state := interrupt.Disable() + p.pwm.Set(p.ch, uint32(volts)) + interrupt.Restore(state) + volatile.StoreUint32(&p.v, math.Float32bits(v)) +} + +func (p *picoPwm) Get() float32 { + return math.Float32frombits(volatile.LoadUint32(&p.v)) +} + +//============= Display =============// + +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) displayProvider { + 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() +} + +//============= RND =============// + +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 +} + +//============= Init =============// + +func init() { + machine.InitADC() + + hwDigital1Input = newDigitalInput(newPicoDigitalReader(machine.GPIO22)) + hwAnalog1Input = newAnalogInput(newPicoAdc(machine.ADC0)) + hwDisplay1Output = newDisplayOutput(newPicoDisplayOutput(machine.I2C0, machine.GPIO0, machine.GPIO1)) + hwButton1Input = newDigitalInput(newPicoDigitalReader(machine.GPIO4)) + hwButton2Input = newDigitalInput(newPicoDigitalReader(machine.GPIO5)) + hwKnob1Input = newAnalogInput(newPicoAdc(machine.ADC1)) + hwKnob2Input = newAnalogInput(newPicoAdc(machine.ADC2)) + hwCV1Output = newVoltageOuput(newPicoPwm(machine.PWM2, machine.GPIO21)) + hwCV2Output = newVoltageOuput(newPicoPwm(machine.PWM2, machine.GPIO20)) + hwCV3Output = newVoltageOuput(newPicoPwm(machine.PWM0, machine.GPIO16)) + hwCV4Output = newVoltageOuput(newPicoPwm(machine.PWM0, machine.GPIO17)) + hwCV5Output = newVoltageOuput(newPicoPwm(machine.PWM1, machine.GPIO18)) + hwCV6Output = newVoltageOuput(newPicoPwm(machine.PWM1, machine.GPIO19)) + hwRandom1Generator = newRandomGenerator(&picoRnd{}) +} diff --git a/internal/hardware/rev1/platform.go b/internal/hardware/rev1/platform.go new file mode 100644 index 0000000..d5b36be --- /dev/null +++ b/internal/hardware/rev1/platform.go @@ -0,0 +1,57 @@ +package rev1 + +import ( + "github.com/heucuva/europi/internal/hardware/hal" +) + +var ( + hwDigital1Input hal.DigitalInput + hwAnalog1Input hal.AnalogInput + hwDisplay1Output hal.DisplayOutput + hwButton1Input hal.ButtonInput + hwButton2Input hal.ButtonInput + hwKnob1Input hal.KnobInput + hwKnob2Input hal.KnobInput + hwCV1Output hal.VoltageOutput + hwCV2Output hal.VoltageOutput + hwCV3Output hal.VoltageOutput + hwCV4Output hal.VoltageOutput + hwCV5Output hal.VoltageOutput + hwCV6Output hal.VoltageOutput + hwRandom1Generator hal.RandomGenerator +) + +func GetHardware(hw hal.HardwareId) any { + switch hw { + case hal.HardwareIdDigital1Input: + return hwDigital1Input + case hal.HardwareIdAnalog1Input: + return hwAnalog1Input + case hal.HardwareIdDisplay1Output: + return hwDisplay1Output + case hal.HardwareIdButton1Input: + return hwButton1Input + case hal.HardwareIdButton2Input: + return hwButton2Input + case hal.HardwareIdKnob1Input: + return hwKnob1Input + case hal.HardwareIdKnob2Input: + return hwKnob2Input + case hal.HardwareIdVoltage1Output: + return hwCV1Output + case hal.HardwareIdVoltage2Output: + return hwCV2Output + case hal.HardwareIdVoltage3Output: + return hwCV3Output + case hal.HardwareIdVoltage4Output: + return hwCV4Output + case hal.HardwareIdVoltage5Output: + return hwCV5Output + case hal.HardwareIdVoltage6Output: + return hwCV6Output + case hal.HardwareIdRandom1Generator: + return hwRandom1Generator + default: + return nil + } +} diff --git a/internal/hardware/rev1/randomgenerator.go b/internal/hardware/rev1/randomgenerator.go new file mode 100644 index 0000000..bb7b127 --- /dev/null +++ b/internal/hardware/rev1/randomgenerator.go @@ -0,0 +1,29 @@ +package rev1 + +import ( + "github.com/heucuva/europi/internal/hardware/hal" +) + +type randomGenerator struct { + rnd rndProvider +} + +func newRandomGenerator(rnd rndProvider) hal.RandomGenerator { + return &randomGenerator{ + rnd: rnd, + } +} + +type rndProvider interface { + Configure(config hal.RandomGeneratorConfig) error +} + +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/internal/hardware/rev1/voltageoutput.go b/internal/hardware/rev1/voltageoutput.go new file mode 100644 index 0000000..4ec15ad --- /dev/null +++ b/internal/hardware/rev1/voltageoutput.go @@ -0,0 +1,93 @@ +package rev1 + +import ( + "fmt" + "time" + + "github.com/heucuva/europi/clamp" + "github.com/heucuva/europi/internal/hardware/hal" + "github.com/heucuva/europi/units" +) + +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. +var defaultPeriod time.Duration = time.Nanosecond * 500 + +// voltageoutput is struct for interacting with the CV/VOct voltage output jacks. +type voltageoutput struct { + pwm pwmProvider + ofs uint16 +} + +// NewOutput returns a new Output interface. +func newVoltageOuput(pwm pwmProvider) hal.VoltageOutput { + o := &voltageoutput{ + pwm: pwm, + } + err := o.Configure(hal.VoltageOutputConfig{ + Period: defaultPeriod, + Offset: CalibratedOffset, + Top: CalibratedTop, + }) + if err != nil { + panic(fmt.Errorf("configuration error: %v", err.Error())) + } + + return o +} + +type pwmProvider interface { + Configure(config hal.VoltageOutputConfig) error + Set(v float32, ofs uint16) + Get() float32 +} + +func (o *voltageoutput) Configure(config hal.VoltageOutputConfig) error { + if err := o.pwm.Configure(config); err != nil { + return err + } + + o.ofs = config.Offset + + return nil +} + +// SetVoltage sets the current output voltage within a range of 0.0 to 10.0. +func (o *voltageoutput) SetVoltage(v float32) { + v = clamp.Clamp(v, MinOutputVoltage, MaxOutputVoltage) + o.pwm.Set(v/MaxOutputVoltage, o.ofs) +} + +// SetCV sets the current output voltage based on a CV value +func (o *voltageoutput) SetCV(cv units.CV) { + 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() * MaxOutputVoltage +} + +func (o *voltageoutput) MinVoltage() float32 { + return MinOutputVoltage +} + +func (o *voltageoutput) MaxVoltage() float32 { + return MaxOutputVoltage +} diff --git a/internal/hardware/revision.go b/internal/hardware/revision.go new file mode 100644 index 0000000..de55060 --- /dev/null +++ b/internal/hardware/revision.go @@ -0,0 +1,16 @@ +package hardware + +type Revision int + +const ( + Revision0 = Revision(iota) + Revision1 + Revision2 +) + +// aliases +const ( + EuroPiProto = Revision0 + EuroPi = Revision1 + EuroPiX = Revision2 +) diff --git a/internal/projects/clockgenerator/clockgenerator.go b/internal/projects/clockgenerator/clockgenerator.go index 1ea4a67..30c475d 100644 --- a/internal/projects/clockgenerator/clockgenerator.go +++ b/internal/projects/clockgenerator/clockgenerator.go @@ -25,11 +25,11 @@ func startLoop(e *europi.EuroPi) { BPM: 120.0, GateDuration: time.Millisecond * 100, Enabled: true, - ClockOut: func(high bool) { - if high { - e.CV1.On() + ClockOut: func(value bool) { + if value { + e.CV1.SetCV(1.0) } else { - e.CV1.Off() + e.CV1.SetCV(0.0) } europi.ForceRepaintUI(e) }, diff --git a/internal/projects/clockgenerator/module/module.go b/internal/projects/clockgenerator/module/module.go index f5ac81f..3b8146f 100644 --- a/internal/projects/clockgenerator/module/module.go +++ b/internal/projects/clockgenerator/module/module.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/clamp" ) type ClockGenerator struct { @@ -74,7 +74,7 @@ func (m *ClockGenerator) SetGateDuration(dur time.Duration) { dur = DefaultGateDuration } - m.gateDuration = europim.Clamp(dur, time.Microsecond, m.interval-time.Microsecond) + m.gateDuration = clamp.Clamp(dur, time.Microsecond, m.interval-time.Microsecond) } func (m *ClockGenerator) GateDuration() time.Duration { diff --git a/internal/projects/clockgenerator/module/setting_bpm.go b/internal/projects/clockgenerator/module/setting_bpm.go index 9d40cb1..10e7dc7 100644 --- a/internal/projects/clockgenerator/module/setting_bpm.go +++ b/internal/projects/clockgenerator/module/setting_bpm.go @@ -3,7 +3,7 @@ package module import ( "fmt" - europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/lerp" "github.com/heucuva/europi/units" ) @@ -12,14 +12,18 @@ const ( MaxBPM float32 = 480.0 ) +var ( + bpmLerp = lerp.NewLerp32(MinBPM, MaxBPM) +) + func BPMString(bpm float32) string { return fmt.Sprintf(`%3.1f`, bpm) } func BPMToCV(bpm float32) units.CV { - return units.CV(europim.InverseLerp(bpm, MinBPM, MaxBPM)) + return units.CV(bpmLerp.ClampedInverseLerp(bpm)) } func CVToBPM(cv units.CV) float32 { - return europim.LerpRound(cv.ToFloat32(), MinBPM, MaxBPM) + return bpmLerp.ClampedLerpRound(cv.ToFloat32()) } diff --git a/internal/projects/clockgenerator/module/setting_gateduration.go b/internal/projects/clockgenerator/module/setting_gateduration.go index 2c5fd3e..db76265 100644 --- a/internal/projects/clockgenerator/module/setting_gateduration.go +++ b/internal/projects/clockgenerator/module/setting_gateduration.go @@ -3,7 +3,7 @@ package module import ( "time" - europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/lerp" "github.com/heucuva/europi/units" ) @@ -12,14 +12,18 @@ const ( MaxGateDuration time.Duration = time.Millisecond * 990 ) +var ( + gateDurationLerp = lerp.NewLerp32(MinGateDuration, MaxGateDuration) +) + func GateDurationString(dur time.Duration) string { return units.DurationString(dur) } func GateDurationToCV(dur time.Duration) units.CV { - return units.CV(europim.InverseLerp(dur, MinGateDuration, MaxGateDuration)) + return units.CV(gateDurationLerp.ClampedInverseLerp(dur)) } func CVToGateDuration(cv units.CV) time.Duration { - return europim.Lerp[time.Duration](cv.ToFloat32(), MinGateDuration, MaxGateDuration) + return gateDurationLerp.ClampedLerpRound(cv.ToFloat32()) } diff --git a/internal/projects/clockgenerator/screen/main.go b/internal/projects/clockgenerator/screen/main.go index d9cc00c..7eaca6c 100644 --- a/internal/projects/clockgenerator/screen/main.go +++ b/internal/projects/clockgenerator/screen/main.go @@ -2,16 +2,19 @@ package screen import ( "fmt" - "machine" "time" "github.com/heucuva/europi" + "github.com/heucuva/europi/experimental/draw" + "github.com/heucuva/europi/experimental/fontwriter" "github.com/heucuva/europi/internal/projects/clockgenerator/module" - "github.com/heucuva/europi/output" + "tinygo.org/x/tinydraw" + "tinygo.org/x/tinyfont/proggy" ) type Main struct { - Clock *module.ClockGenerator + Clock *module.ClockGenerator + writer fontwriter.Writer } const ( @@ -19,22 +22,29 @@ const ( line2y int16 = 23 ) +var ( + DefaultFont = &proggy.TinySZ8pt7b +) + func (m *Main) Start(e *europi.EuroPi) { + m.writer = fontwriter.Writer{ + Display: e.Display, + Font: DefaultFont, + } } func (m *Main) Button1Debounce() time.Duration { return time.Millisecond * 200 } -func (m *Main) Button1(e *europi.EuroPi, p machine.Pin) { +func (m *Main) Button1(e *europi.EuroPi, deltaTime time.Duration) { m.Clock.Toggle() } func (m *Main) Paint(e *europi.EuroPi, deltaTime time.Duration) { - disp := e.Display if m.Clock.Enabled() { - disp.DrawHLine(0, 0, 7, output.White) + tinydraw.Line(e.Display, 0, 0, 7, 0, draw.White) } - disp.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y) - disp.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y) + m.writer.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y, draw.White) + m.writer.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y, draw.White) } diff --git a/internal/projects/clockgenerator/screen/settings.go b/internal/projects/clockgenerator/screen/settings.go index e645fa3..7c53d98 100644 --- a/internal/projects/clockgenerator/screen/settings.go +++ b/internal/projects/clockgenerator/screen/settings.go @@ -1,7 +1,6 @@ package screen import ( - "machine" "time" "github.com/heucuva/europi" @@ -55,7 +54,7 @@ func (m *Settings) Button1Debounce() time.Duration { return time.Millisecond * 200 } -func (m *Settings) Button1(e *europi.EuroPi, p machine.Pin) { +func (m *Settings) Button1(e *europi.EuroPi, deltaTime time.Duration) { m.km.Next() } diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index c57e43b..0811bc1 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -33,8 +33,10 @@ import ( "time" "tinygo.org/x/tinydraw" + "tinygo.org/x/tinyfont/proggy" "github.com/heucuva/europi" + "github.com/heucuva/europi/experimental/fontwriter" europim "github.com/heucuva/europi/math" "github.com/heucuva/europi/output" ) @@ -51,6 +53,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() { @@ -76,6 +79,7 @@ type Clockwerk struct { period time.Duration clocks [6]int resets [6]chan uint8 + writer fontwriter.Writer *europi.EuroPi } @@ -221,6 +225,10 @@ func startLoop(e *europi.EuroPi) { app.EuroPi = e app.clocks = DefaultFactor app.displayShouldUpdate = true + app.writer = fontwriter.Writer{ + Display: e.Display, + Font: DefaultFont, + } // Lower range value can have lower sample size app.K1.Samples(500) diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index 778b754..44dd3ca 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -3,14 +3,14 @@ package main import ( "fmt" - "machine" "time" "tinygo.org/x/tinydraw" + "tinygo.org/x/tinyfont/proggy" "github.com/heucuva/europi" - "github.com/heucuva/europi/input" - "github.com/heucuva/europi/output" + "github.com/heucuva/europi/experimental/draw" + "github.com/heucuva/europi/experimental/fontwriter" ) type MyApp struct { @@ -27,51 +27,60 @@ func startLoop(e *europi.EuroPi) { myApp.staticCv = 5 // Demonstrate adding a IRQ handler to B1 and B2. - e.B1.Handler(func(p machine.Pin) { + e.B1.Handler(func(value bool, deltaTime time.Duration) { myApp.knobsDisplayPercent = !myApp.knobsDisplayPercent }) - e.B2.Handler(func(p machine.Pin) { - myApp.staticCv = (myApp.staticCv + 1) % input.MaxVoltage + e.B2.Handler(func(value bool, deltaTime time.Duration) { + myApp.staticCv = (myApp.staticCv + 1) % int(e.K1.MaxVoltage()) }) } +var ( + DefaultFont = &proggy.TinySZ8pt7b +) + func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { e.Display.ClearBuffer() // Highlight the border of the oled display. - tinydraw.Rectangle(e.Display, 0, 0, 128, 32, output.White) + tinydraw.Rectangle(e.Display, 0, 0, 128, 32, draw.White) + + writer := fontwriter.Writer{ + Display: e.Display, + Font: DefaultFont, + } // 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) + 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: %2d K2: %2d", e.K1.Range(100), e.K2.Range(100)) + knobText = fmt.Sprintf("K1: %3d K2: %3d", int(e.K1.Percent()*100), int(e.K2.Percent()*100)) } - e.Display.WriteLine(knobText, 3, 18) + writer.WriteLine(knobText, 3, 18, draw.White) // Show current button press state. - e.Display.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", e.B1.Value(), e.B2.Value()), 3, 28) + writer.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", e.B1.Value(), e.B2.Value()), 3, 28, draw.White) e.Display.Display() // Set voltage values for the 6 CV outputs. - if e.K1.Range(1<<12) != myApp.prevK1 { + if kv := uint16(e.K1.Percent() * float32(1<<12)); kv != myApp.prevK1 { e.CV1.SetVoltage(e.K1.ReadVoltage()) - e.CV4.SetVoltage(output.MaxVoltage - e.K1.ReadVoltage()) - myApp.prevK1 = e.K1.Range(1 << 12) + e.CV4.SetVoltage(e.CV4.MaxVoltage() - e.K1.ReadVoltage()) + myApp.prevK1 = kv } - if e.K2.Range(1<<12) != myApp.prevK2 { + if kv := uint16(e.K2.Percent() * float32(1<<12)); kv != myApp.prevK2 { e.CV2.SetVoltage(e.K2.ReadVoltage()) - e.CV5.SetVoltage(output.MaxVoltage - e.K2.ReadVoltage()) - myApp.prevK2 = e.K2.Range(1 << 12) + e.CV5.SetVoltage(e.CV5.MaxVoltage() - e.K2.ReadVoltage()) + myApp.prevK2 = kv } - e.CV3.On() + e.CV3.SetCV(1.0) if myApp.staticCv != myApp.prevStaticCv { e.CV6.SetVoltage(float32(myApp.staticCv)) myApp.prevStaticCv = myApp.staticCv diff --git a/internal/projects/randomskips/module/module.go b/internal/projects/randomskips/module/module.go index 7e766f5..b7853c1 100644 --- a/internal/projects/randomskips/module/module.go +++ b/internal/projects/randomskips/module/module.go @@ -32,13 +32,13 @@ func (m *RandomSkips) Init(config Config) error { func noopGate(high bool) { } -func (m *RandomSkips) Gate(high bool) { +func (m *RandomSkips) Gate(value bool) { prev := m.active lastInput := m.lastInput next := prev - m.lastInput = high + m.lastInput = value - if high != lastInput && rand.Float32() < m.ac { + if value != lastInput && rand.Float32() < m.ac { next = !prev } diff --git a/internal/projects/randomskips/module/setting_chance.go b/internal/projects/randomskips/module/setting_chance.go index a386511..c7a83b7 100644 --- a/internal/projects/randomskips/module/setting_chance.go +++ b/internal/projects/randomskips/module/setting_chance.go @@ -3,7 +3,7 @@ package module import ( "fmt" - europim "github.com/heucuva/europi/math" + "github.com/heucuva/europi/clamp" "github.com/heucuva/europi/units" ) @@ -16,5 +16,5 @@ func ChanceToCV(chance float32) units.CV { } func CVToChance(cv units.CV) float32 { - return europim.Clamp(cv.ToFloat32(), 0.0, 1.0) + return clamp.Clamp(cv.ToFloat32(), 0.0, 1.0) } diff --git a/internal/projects/randomskips/randomskips.go b/internal/projects/randomskips/randomskips.go index 18c64d8..76d60ef 100644 --- a/internal/projects/randomskips/randomskips.go +++ b/internal/projects/randomskips/randomskips.go @@ -1,16 +1,15 @@ package main import ( - "machine" "time" "github.com/heucuva/europi" "github.com/heucuva/europi/experimental/screenbank" + "github.com/heucuva/europi/internal/hardware/hal" clockgenerator "github.com/heucuva/europi/internal/projects/clockgenerator/module" clockScreen "github.com/heucuva/europi/internal/projects/clockgenerator/screen" "github.com/heucuva/europi/internal/projects/randomskips/module" "github.com/heucuva/europi/internal/projects/randomskips/screen" - "github.com/heucuva/europi/output" ) var ( @@ -30,12 +29,12 @@ var ( } ) -func makeGate(out output.Output) func(high bool) { - return func(high bool) { - if high { - out.On() +func makeGate(out hal.VoltageOutput) func(value bool) { + return func(value bool) { + if value { + out.SetCV(1.0) } else { - out.Off() + out.SetCV(0.0) } } } @@ -56,9 +55,8 @@ func startLoop(e *europi.EuroPi) { panic(err) } - e.DI.HandlerEx(machine.PinRising|machine.PinFalling, func(p machine.Pin) { - high := e.DI.Value() - skip.Gate(high) + e.DI.HandlerEx(hal.ChangeAny, func(value bool, _ time.Duration) { + skip.Gate(value) }) } diff --git a/internal/projects/randomskips/screen/main.go b/internal/projects/randomskips/screen/main.go index 392c7f9..e6205aa 100644 --- a/internal/projects/randomskips/screen/main.go +++ b/internal/projects/randomskips/screen/main.go @@ -2,18 +2,21 @@ package screen import ( "fmt" - "machine" "time" "github.com/heucuva/europi" + "github.com/heucuva/europi/experimental/draw" + "github.com/heucuva/europi/experimental/fontwriter" clockgenerator "github.com/heucuva/europi/internal/projects/clockgenerator/module" "github.com/heucuva/europi/internal/projects/randomskips/module" - "github.com/heucuva/europi/output" + "tinygo.org/x/tinydraw" + "tinygo.org/x/tinyfont/proggy" ) type Main struct { RandomSkips *module.RandomSkips Clock *clockgenerator.ClockGenerator + writer fontwriter.Writer } const ( @@ -21,22 +24,29 @@ const ( line2y int16 = 23 ) +var ( + DefaultFont = &proggy.TinySZ8pt7b +) + func (m *Main) Start(e *europi.EuroPi) { + m.writer = fontwriter.Writer{ + Display: e.Display, + Font: DefaultFont, + } } func (m *Main) Button1Debounce() time.Duration { return time.Millisecond * 200 } -func (m *Main) Button1(e *europi.EuroPi, p machine.Pin) { +func (m *Main) Button1(e *europi.EuroPi, deltaTime time.Duration) { m.Clock.Toggle() } func (m *Main) Paint(e *europi.EuroPi, deltaTime time.Duration) { - disp := e.Display if m.Clock.Enabled() { - disp.DrawHLine(0, 0, 7, output.White) + tinydraw.Line(e.Display, 0, 0, 7, 0, draw.White) } - disp.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y) - disp.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y) + m.writer.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y, draw.White) + m.writer.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y, draw.White) } diff --git a/internal/projects/randomskips/screen/settings.go b/internal/projects/randomskips/screen/settings.go index dfcd7d3..25c31da 100644 --- a/internal/projects/randomskips/screen/settings.go +++ b/internal/projects/randomskips/screen/settings.go @@ -1,7 +1,6 @@ package screen import ( - "machine" "time" "github.com/heucuva/europi" @@ -42,7 +41,7 @@ func (m *Settings) Button1Debounce() time.Duration { return time.Millisecond * 200 } -func (m *Settings) Button1(e *europi.EuroPi, p machine.Pin) { +func (m *Settings) Button1(e *europi.EuroPi, deltaTime time.Duration) { m.km.Next() } diff --git a/lerp/lerp.go b/lerp/lerp.go new file mode 100644 index 0000000..902cef0 --- /dev/null +++ b/lerp/lerp.go @@ -0,0 +1,22 @@ +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 +} + +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..1c0fcb7 --- /dev/null +++ b/lerp/lerp32.go @@ -0,0 +1,45 @@ +package lerp + +import "github.com/heucuva/europi/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-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-l.b)/l.r, 0.0, 1.0) + } + return 0.0 +} diff --git a/lerp/lerp64.go b/lerp/lerp64.go new file mode 100644 index 0000000..1791ac1 --- /dev/null +++ b/lerp/lerp64.go @@ -0,0 +1,45 @@ +package lerp + +import "github.com/heucuva/europi/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-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-l.b)/l.r, 0.0, 1.0) + } + return 0.0 +} diff --git a/math/lerp.go b/math/lerp.go deleted file mode 100644 index 3c572d6..0000000 --- a/math/lerp.go +++ /dev/null @@ -1,23 +0,0 @@ -package math - -import "math" - -type Lerpable interface { - ~uint8 | ~uint16 | ~int | ~float32 | ~int32 | ~int64 -} - -func Lerp[V Lerpable](t float32, low, high V) V { - return V(t*float32(high-low)) + low -} - -func LerpRound[V Lerpable](t float32, low, high V) V { - l := math.Round(float64(t) * float64(high-low)) - return Clamp(V(l)+low, low, high) -} - -func InverseLerp[V Lerpable](v, low, high V) float32 { - if high == low { - return 0 - } - return float32(v-low) / float32(high-low) -} diff --git a/math/math.go b/math/math.go deleted file mode 100644 index 2b59759..0000000 --- a/math/math.go +++ /dev/null @@ -1,25 +0,0 @@ -package math - -type Clampable interface { - ~uint8 | ~uint16 | ~int | ~float32 | ~int32 | ~int64 -} - -// Clamp returns a value that is no lower than "low" and no higher than "high". -func Clamp[V Clampable](value, low, high V) V { - if value >= high { - return high - } - if value <= low { - return low - } - return value -} - -// Abs returns the absolute value -func Abs(value float32) float32 { - if value >= 0 { - return value - } - - return -value -} diff --git a/output/display.go b/output/display.go deleted file mode 100644 index 337320c..0000000 --- a/output/display.go +++ /dev/null @@ -1,155 +0,0 @@ -package output - -import ( - "image/color" - "machine" - - "tinygo.org/x/drivers/ssd1306" - "tinygo.org/x/tinydraw" - "tinygo.org/x/tinyfont" - "tinygo.org/x/tinyfont/notoemoji" - "tinygo.org/x/tinyfont/proggy" -) - -const ( - OLEDFreq = machine.KHz * 400 - OLEDAddr = ssd1306.Address_128_32 - OLEDWidth = 128 - OLEDHeight = 32 -) - -var ( - DefaultChannel = machine.I2C0 - DefaultFont = &proggy.TinySZ8pt7b - DefaultEmojiFont = ¬oemoji.NotoEmojiRegular12pt - White = color.RGBA{255, 255, 255, 255} - Black = color.RGBA{0, 0, 0, 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} -} - -// SetFont overrides the default font used by `WriteLine`. -func (d *Display) SetFont(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) { - d.WriteLineAligned(text, x, y, AlignLeft, AlignTop) -} - -// WriteEmojiLineAligned writes the given emoji text to the display where: -// x, y is the bottom leftmost pixel of the text -// alignh is horizontal alignment -// alignv is vertical alignment -func (d *Display) WriteEmojiLineAligned(text string, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { - d.writeLineAligned(text, d.font, x, y, alignh, alignv) -} - -// 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 (d *Display) WriteLineAligned(text string, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { - d.writeLineAligned(text, d.font, x, y, alignh, alignv) -} - -func (d *Display) writeLineAligned(text string, font tinyfont.Fonter, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { - x0, y0 := x, y - switch alignh { - case AlignLeft: - case AlignCenter: - _, outerWidth := tinyfont.LineWidth(font, text) - x0 = (OLEDWidth-int16(outerWidth))/2 - x - case AlignRight: - _, outerWidth := tinyfont.LineWidth(font, text) - x0 = OLEDWidth - int16(outerWidth) - x - default: - panic("invalid alignment") - } - tinyfont.WriteLine(d, font, x0, y0, text, White) -} - -// WriteLineInverse writes the given text to the display in an inverted way where x, y is the bottom leftmost pixel of the text -func (d *Display) WriteLineInverse(text string, x, y int16) { - d.WriteLineInverseAligned(text, x, y, AlignLeft, AlignTop) -} - -// 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 (d *Display) WriteLineInverseAligned(text string, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { - d.writeLineInverseAligned(text, d.font, x, y, alignh, alignv) -} - -// WriteEmojiLineInverseAligned writes the given emoji 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 (d *Display) WriteEmojiLineInverseAligned(text string, x, y int16, alignh HorizontalAlignment, alignv VerticalAlignment) { - d.writeLineInverseAligned(text, DefaultEmojiFont, x, y, alignh, alignv) -} - -func (d *Display) writeLineInverseAligned(text string, font tinyfont.Fonter, x, y int16, 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: - x0 = (OLEDWidth-int16(outerWidth))/2 - x - x1 = x0 + 1 - case AlignRight: - x0 = OLEDWidth - int16(outerWidth) - x - x1 = x0 + 1 - default: - panic("invalid alignment") - } - switch alignv { - case AlignTop: - case AlignMiddle: - midY := (OLEDHeight - outerHeight) / 2 - y0 += midY - y1 += midY - case AlignBottom: - y1 = OLEDHeight - y1 - y0 = y1 - outerHeight + 2 - default: - panic("invalid alignment") - } - tinydraw.FilledRectangle(d, x0, y0, int16(outerWidth+2), outerHeight, White) - tinyfont.WriteLine(d, font, x1, y1, text, Black) -} - -// DrawHLine draws a horizontal line -func (d *Display) DrawHLine(x, y, xLen int16, c color.RGBA) { - tinydraw.Line(d, x, y, x+xLen-1, y, c) -} - -// DrawLine draws an arbitrary line -func (d *Display) DrawLine(x0, y0, x1, y1 int16, c color.RGBA) { - tinydraw.Line(d, x0, y0, x1, y1, c) -} diff --git a/output/output.go b/output/output.go deleted file mode 100644 index 6df9684..0000000 --- a/output/output.go +++ /dev/null @@ -1,126 +0,0 @@ -package output - -import ( - "log" - "machine" - "math" - "runtime/interrupt" - "runtime/volatile" - - europim "github.com/heucuva/europi/math" - "github.com/heucuva/europi/units" -) - -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 - - MaxVoltage = 10.0 - MinVoltage = 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. -var defaultPeriod uint64 = 500 - -// Output is an interface for interacting with the cv output jacks. -type Output interface { - Get() uint32 - SetVoltage(v float32) - SetCV(cv units.CV) - SetVOct(voct units.VOct) - Set(v bool) - On() - Off() - Voltage() float32 -} - -// Output is struct for interacting with the cv output jacks. -type output struct { - pwm PWM - pin machine.Pin - ch uint8 - v uint32 -} - -// NewOutput returns a new Output interface. -func NewOutput(pin machine.Pin, pwm PWM) 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, MinVoltage} -} - -// Get returns the current set voltage in the range of 0 to pwm.Top(). -func (o *output) Get() uint32 { - state := interrupt.Disable() - v := o.pwm.Get(o.ch) - interrupt.Restore(state) - return v -} - -// Set updates the current voltage high (true) or low (false) -func (o *output) Set(v bool) { - if v { - o.On() - } else { - o.Off() - } -} - -// SetVoltage sets the current output voltage within a range of 0.0 to 10.0. -func (o *output) SetVoltage(v float32) { - v = europim.Clamp(v, MinVoltage, MaxVoltage) - invertedCv := (v / MaxVoltage) * float32(o.pwm.Top()) - // cv := (float32(o.pwm.Top()) - invertedCv) - CalibratedOffset - cv := float32(invertedCv) - CalibratedOffset - state := interrupt.Disable() - o.pwm.Set(o.ch, uint32(cv)) - interrupt.Restore(state) - volatile.StoreUint32(&o.v, math.Float32bits(v)) -} - -// SetCV sets the current output voltage based on a CV value -func (o *output) SetCV(cv units.CV) { - o.SetVoltage(cv.ToVolts()) -} - -// SetCV sets the current output voltage based on a V/Octave value -func (o *output) SetVOct(voct units.VOct) { - o.SetVoltage(voct.ToVolts()) -} - -// On sets the current voltage high at 10.0v. -func (o *output) On() { - volatile.StoreUint32(&o.v, math.Float32bits(MaxVoltage)) - state := interrupt.Disable() - o.pwm.Set(o.ch, o.pwm.Top()) - interrupt.Restore(state) -} - -// Off sets the current voltage low at 0.0v. -func (o *output) Off() { - volatile.StoreUint32(&o.v, math.Float32bits(MinVoltage)) - state := interrupt.Disable() - o.pwm.Set(o.ch, 0) - interrupt.Restore(state) -} - -// Voltage returns the current voltage -func (o *output) Voltage() float32 { - return math.Float32frombits(volatile.LoadUint32(&o.v)) -} diff --git a/output/pwm.go b/output/pwm.go deleted file mode 100644 index 7aa8759..0000000 --- a/output/pwm.go +++ /dev/null @@ -1,16 +0,0 @@ -package output - -import ( - "machine" -) - -// PWM is an interface for interacting with a machine.pwmGroup -type PWM 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 -} From 74a3ac92b0e8a5c2e7332b7be171279908be7d5a Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 22 Apr 2023 23:35:14 -0700 Subject: [PATCH 12/62] Allow setting just sampling size on analog input --- internal/hardware/rev1/analoginput.go | 6 ++- internal/projects/clockwerk/clockwerk.go | 50 +++++++++++++++--------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/internal/hardware/rev1/analoginput.go b/internal/hardware/rev1/analoginput.go index dd429f4..e280adf 100644 --- a/internal/hardware/rev1/analoginput.go +++ b/internal/hardware/rev1/analoginput.go @@ -47,6 +47,11 @@ func (a *analoginput) Configure(config hal.AnalogInputConfig) error { return errors.New("samples must be non-zero") } + if config.CalibratedMinAI == 0 && config.CalibratedMaxAI == 0 { + config.CalibratedMinAI = DefaultCalibratedMinAI + config.CalibratedMaxAI = DefaultCalibratedMaxAI + } + if config.CalibratedMinAI == config.CalibratedMaxAI { return errors.New("calibratedminai and calibratedmaxai must be different") } else if config.CalibratedMinAI > config.CalibratedMaxAI { @@ -55,7 +60,6 @@ func (a *analoginput) Configure(config hal.AnalogInputConfig) error { a.samples = config.Samples a.cal = lerp.NewLerp32(config.CalibratedMinAI, config.CalibratedMaxAI) - return nil } diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index 0811bc1..1790be7 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -28,7 +28,6 @@ package main import ( - "machine" "strconv" "time" @@ -36,9 +35,11 @@ import ( "tinygo.org/x/tinyfont/proggy" "github.com/heucuva/europi" + "github.com/heucuva/europi/clamp" + "github.com/heucuva/europi/experimental/draw" "github.com/heucuva/europi/experimental/fontwriter" - europim "github.com/heucuva/europi/math" - "github.com/heucuva/europi/output" + "github.com/heucuva/europi/internal/hardware/hal" + "github.com/heucuva/europi/lerp" ) const ( @@ -82,6 +83,8 @@ type Clockwerk struct { writer fontwriter.Writer *europi.EuroPi + bpmLerp lerp.Lerper32[uint16] + factorLerp lerp.Lerper32[int] } func (c *Clockwerk) editParams() { @@ -101,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 @@ -116,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() { @@ -158,11 +162,11 @@ 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())) - c.CV[i].Off() + c.CV[i].SetCV(0.0) t = t.Add(low) time.Sleep(t.Sub(time.Now())) } @@ -198,9 +202,11 @@ func (c *Clockwerk) updateDisplay() { 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.Display.Size() + divWidth := int(dispWidth) / len(c.clocks) for i, factor := range c.clocks { text := " 1" switch { @@ -209,12 +215,12 @@ func (c *Clockwerk) updateDisplay() { case factor > 1: text = "x" + strconv.Itoa(factor) } - c.Display.WriteLine(text, int16(i*output.OLEDWidth/len(c.clocks))+2, 26) + c.writer.WriteLine(text, int16(i*divWidth)+2, 26, draw.White) } - xWidth := int16(output.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, output.White) + tinydraw.Rectangle(c.Display, xOffset, 16, xWidth, 16, draw.White) c.Display.Display() } @@ -229,33 +235,39 @@ func startLoop(e *europi.EuroPi) { Display: e.Display, 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 - app.K1.Samples(500) - app.K2.Samples(20) + app.K1.Configure(hal.AnalogInputConfig{ + Samples: 500, + }) + app.K2.Configure(hal.AnalogInputConfig{ + Samples: 20, + }) - app.DI.Handler(func(pin machine.Pin) { + app.DI.Handler(func(_ bool, deltaTime time.Duration) { // Measure current period between clock pulses. - app.period = time.Now().Sub(app.DI.LastInput()) + app.period = deltaTime }) // Move clock config option to the left. - app.B1.Handler(func(p machine.Pin) { + app.B1.Handler(func(_ bool, deltaTime time.Duration) { if app.B2.Value() { app.doClockReset = true return } - app.selected = uint8(europim.Clamp(int(app.selected)-1, 0, len(app.clocks))) + app.selected = uint8(clamp.Clamp(int(app.selected)-1, 0, len(app.clocks))) app.displayShouldUpdate = true }) // Move clock config option to the right. - app.B2.Handler(func(p machine.Pin) { + app.B2.Handler(func(_ bool, deltaTime time.Duration) { if app.B1.Value() { app.doClockReset = true return } - app.selected = uint8(europim.Clamp(int(app.selected)+1, 0, len(app.clocks)-1)) + app.selected = uint8(clamp.Clamp(int(app.selected)+1, 0, len(app.clocks)-1)) app.displayShouldUpdate = true }) From 1dfa0e98f853302ab2788105241d4d5dd8a9aba0 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 22 Apr 2023 23:42:28 -0700 Subject: [PATCH 13/62] Reduce to minimum viable startup --- internal/projects/clockwerk/clockwerk.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index 1790be7..0178b4f 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -278,7 +278,7 @@ func startLoop(e *europi.EuroPi) { app.startClocks() } -func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { +func mainLoop() { if app.doClockReset { app.doClockReset = false app.resetClocks() @@ -288,9 +288,13 @@ func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { } func main() { - europi.Bootstrap( - europi.StartLoop(startLoop), - europi.MainLoop(mainLoop), - europi.MainLoopInterval(ResetDelay), // Check for clock updates every 2 seconds. - ) + startLoop(europi.New()) + + // Check for clock updates every 2 seconds. + ticker := time.NewTicker(ResetDelay) + defer ticker.Stop() + for { + <-ticker.C + mainLoop() + } } From 315d11d2baf9fc9ab6b22d7fbabd94fe9c939dbb Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 00:38:49 -0700 Subject: [PATCH 14/62] add simple revision detection - also allow users to access hardware revision singletons as they please --- internal/hardware/hal/hal.go | 1 + internal/hardware/hal/revisionmarker.go | 14 +++++ internal/hardware/platform.go | 18 ++++-- internal/hardware/rev1/analoginput.go | 17 +++-- internal/hardware/rev1/nonpico.go | 29 ++++----- internal/hardware/rev1/pico.go | 29 ++++----- internal/hardware/rev1/platform.go | 79 +++++++++++++++--------- internal/hardware/rev1/revisionmarker.go | 13 ++++ internal/hardware/revision.go | 11 ++-- 9 files changed, 134 insertions(+), 77 deletions(-) create mode 100644 internal/hardware/hal/revisionmarker.go create mode 100644 internal/hardware/rev1/revisionmarker.go diff --git a/internal/hardware/hal/hal.go b/internal/hardware/hal/hal.go index b0bebff..3ef761c 100644 --- a/internal/hardware/hal/hal.go +++ b/internal/hardware/hal/hal.go @@ -4,6 +4,7 @@ type HardwareId int const ( HardwareIdInvalid = HardwareId(iota) + HardwareIdRevisionMarker HardwareIdDigital1Input HardwareIdAnalog1Input HardwareIdDisplay1Output diff --git a/internal/hardware/hal/revisionmarker.go b/internal/hardware/hal/revisionmarker.go new file mode 100644 index 0000000..30d1e7a --- /dev/null +++ b/internal/hardware/hal/revisionmarker.go @@ -0,0 +1,14 @@ +package hal + +type Revision int + +const ( + RevisionUnknown = Revision(iota) + Revision0 + Revision1 + Revision2 +) + +type RevisionMarker interface { + Revision() Revision +} diff --git a/internal/hardware/platform.go b/internal/hardware/platform.go index 8dd00b8..a7f8724 100644 --- a/internal/hardware/platform.go +++ b/internal/hardware/platform.go @@ -5,19 +5,27 @@ import ( "github.com/heucuva/europi/internal/hardware/rev1" ) -func GetHardware[T any](revision Revision, hw hal.HardwareId) T { +func GetHardware[T any](revision Revision, id hal.HardwareId) T { switch revision { case Revision1: - hw, _ := rev1.GetHardware(hw).(T) - return hw + return rev1.GetHardware[T](id) case Revision2: // TODO: implement hardware design of rev2 - hw, _ := rev1.GetHardware(hw).(T) - return hw + return rev1.GetHardware[T](id) default: var none T return none } } + +func RevisionDetection() Revision { + for i := Revision0; i <= Revision2; i++ { + if rd := GetHardware[hal.RevisionMarker](i, hal.HardwareIdRevisionMarker); rd != nil { + // use the result of the call - don't just use `i` - in the event there's an alias or redirect involved + return rd.Revision() + } + } + return RevisionUnknown +} diff --git a/internal/hardware/rev1/analoginput.go b/internal/hardware/rev1/analoginput.go index e280adf..7f11712 100644 --- a/internal/hardware/rev1/analoginput.go +++ b/internal/hardware/rev1/analoginput.go @@ -47,19 +47,16 @@ func (a *analoginput) Configure(config hal.AnalogInputConfig) error { return errors.New("samples must be non-zero") } - if config.CalibratedMinAI == 0 && config.CalibratedMaxAI == 0 { - config.CalibratedMinAI = DefaultCalibratedMinAI - config.CalibratedMaxAI = DefaultCalibratedMaxAI - } - - if config.CalibratedMinAI == config.CalibratedMaxAI { - return errors.New("calibratedminai and calibratedmaxai must be different") - } else if config.CalibratedMinAI > config.CalibratedMaxAI { - return errors.New("calibtatedminai must be less than calibratedmaxai") + if config.CalibratedMinAI != 0 || config.CalibratedMaxAI != 0 { + if config.CalibratedMinAI == config.CalibratedMaxAI { + return errors.New("calibratedminai and calibratedmaxai must be different") + } else if config.CalibratedMinAI > config.CalibratedMaxAI { + return errors.New("calibtatedminai must be less than calibratedmaxai") + } + a.cal = lerp.NewLerp32(config.CalibratedMinAI, config.CalibratedMaxAI) } a.samples = config.Samples - a.cal = lerp.NewLerp32(config.CalibratedMinAI, config.CalibratedMaxAI) return nil } diff --git a/internal/hardware/rev1/nonpico.go b/internal/hardware/rev1/nonpico.go index 4b0b7f7..c520e48 100644 --- a/internal/hardware/rev1/nonpico.go +++ b/internal/hardware/rev1/nonpico.go @@ -161,18 +161,19 @@ func (d *nonPicoDisplayOutput) Display() error { //============= Init =============// func init() { - hwDigital1Input = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdDigital1Input)) - hwAnalog1Input = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdAnalog1Input)) - hwDisplay1Output = newDisplayOutput(newNonPicoDisplayOutput(DefaultEventBus, hal.HardwareIdDisplay1Output)) - hwButton1Input = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdButton1Input)) - hwButton2Input = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdButton2Input)) - hwKnob1Input = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdKnob1Input)) - hwKnob2Input = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdKnob2Input)) - hwCV1Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage1Output)) - hwCV2Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage2Output)) - hwCV3Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage3Output)) - hwCV4Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage4Output)) - hwCV5Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage5Output)) - hwCV6Output = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage6Output)) - hwRandom1Generator = newRandomGenerator(nil) + RevisionMarker = newRevisionMarker() + InputDigital1 = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdDigital1Input)) + InputAnalog1 = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdAnalog1Input)) + OutputDisplay1 = newDisplayOutput(newNonPicoDisplayOutput(DefaultEventBus, hal.HardwareIdDisplay1Output)) + InputButton1 = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdButton1Input)) + InputButton2 = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdButton2Input)) + InputKnob1 = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdKnob1Input)) + InputKnob2 = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdKnob2Input)) + OutputVoltage1 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage1Output)) + OutputVoltage2 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage2Output)) + OutputVoltage3 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage3Output)) + OutputVoltage4 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage4Output)) + OutputVoltage5 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage5Output)) + OutputVoltage6 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage6Output)) + DeviceRandomGenerator1 = newRandomGenerator(nil) } diff --git a/internal/hardware/rev1/pico.go b/internal/hardware/rev1/pico.go index d8515b2..6560ddd 100644 --- a/internal/hardware/rev1/pico.go +++ b/internal/hardware/rev1/pico.go @@ -216,18 +216,19 @@ func (r *picoRnd) Configure(config hal.RandomGeneratorConfig) error { func init() { machine.InitADC() - hwDigital1Input = newDigitalInput(newPicoDigitalReader(machine.GPIO22)) - hwAnalog1Input = newAnalogInput(newPicoAdc(machine.ADC0)) - hwDisplay1Output = newDisplayOutput(newPicoDisplayOutput(machine.I2C0, machine.GPIO0, machine.GPIO1)) - hwButton1Input = newDigitalInput(newPicoDigitalReader(machine.GPIO4)) - hwButton2Input = newDigitalInput(newPicoDigitalReader(machine.GPIO5)) - hwKnob1Input = newAnalogInput(newPicoAdc(machine.ADC1)) - hwKnob2Input = newAnalogInput(newPicoAdc(machine.ADC2)) - hwCV1Output = newVoltageOuput(newPicoPwm(machine.PWM2, machine.GPIO21)) - hwCV2Output = newVoltageOuput(newPicoPwm(machine.PWM2, machine.GPIO20)) - hwCV3Output = newVoltageOuput(newPicoPwm(machine.PWM0, machine.GPIO16)) - hwCV4Output = newVoltageOuput(newPicoPwm(machine.PWM0, machine.GPIO17)) - hwCV5Output = newVoltageOuput(newPicoPwm(machine.PWM1, machine.GPIO18)) - hwCV6Output = newVoltageOuput(newPicoPwm(machine.PWM1, machine.GPIO19)) - hwRandom1Generator = newRandomGenerator(&picoRnd{}) + RevisionMarker = newRevisionMarker() + InputDigital1 = newDigitalInput(newPicoDigitalReader(machine.GPIO22)) + InputAnalog1 = newAnalogInput(newPicoAdc(machine.ADC0)) + OutputDisplay1 = newDisplayOutput(newPicoDisplayOutput(machine.I2C0, machine.GPIO0, machine.GPIO1)) + InputButton1 = newDigitalInput(newPicoDigitalReader(machine.GPIO4)) + InputButton2 = newDigitalInput(newPicoDigitalReader(machine.GPIO5)) + InputKnob1 = newAnalogInput(newPicoAdc(machine.ADC1)) + InputKnob2 = newAnalogInput(newPicoAdc(machine.ADC2)) + OutputVoltage1 = newVoltageOuput(newPicoPwm(machine.PWM2, machine.GPIO21)) + OutputVoltage2 = newVoltageOuput(newPicoPwm(machine.PWM2, machine.GPIO20)) + OutputVoltage3 = newVoltageOuput(newPicoPwm(machine.PWM0, machine.GPIO16)) + OutputVoltage4 = newVoltageOuput(newPicoPwm(machine.PWM0, machine.GPIO17)) + OutputVoltage5 = newVoltageOuput(newPicoPwm(machine.PWM1, machine.GPIO18)) + OutputVoltage6 = newVoltageOuput(newPicoPwm(machine.PWM1, machine.GPIO19)) + DeviceRandomGenerator1 = newRandomGenerator(&picoRnd{}) } diff --git a/internal/hardware/rev1/platform.go b/internal/hardware/rev1/platform.go index d5b36be..4d1af8a 100644 --- a/internal/hardware/rev1/platform.go +++ b/internal/hardware/rev1/platform.go @@ -5,53 +5,72 @@ import ( ) var ( - hwDigital1Input hal.DigitalInput - hwAnalog1Input hal.AnalogInput - hwDisplay1Output hal.DisplayOutput - hwButton1Input hal.ButtonInput - hwButton2Input hal.ButtonInput - hwKnob1Input hal.KnobInput - hwKnob2Input hal.KnobInput - hwCV1Output hal.VoltageOutput - hwCV2Output hal.VoltageOutput - hwCV3Output hal.VoltageOutput - hwCV4Output hal.VoltageOutput - hwCV5Output hal.VoltageOutput - hwCV6Output hal.VoltageOutput - hwRandom1Generator hal.RandomGenerator + RevisionMarker hal.RevisionMarker + InputDigital1 hal.DigitalInput + InputAnalog1 hal.AnalogInput + OutputDisplay1 hal.DisplayOutput + InputButton1 hal.ButtonInput + InputButton2 hal.ButtonInput + InputKnob1 hal.KnobInput + InputKnob2 hal.KnobInput + OutputVoltage1 hal.VoltageOutput + OutputVoltage2 hal.VoltageOutput + OutputVoltage3 hal.VoltageOutput + OutputVoltage4 hal.VoltageOutput + OutputVoltage5 hal.VoltageOutput + OutputVoltage6 hal.VoltageOutput + DeviceRandomGenerator1 hal.RandomGenerator ) -func GetHardware(hw hal.HardwareId) any { +func GetHardware[T any](hw hal.HardwareId) T { switch hw { + case hal.HardwareIdRevisionMarker: + t, _ := RevisionMarker.(T) + return t case hal.HardwareIdDigital1Input: - return hwDigital1Input + t, _ := InputDigital1.(T) + return t case hal.HardwareIdAnalog1Input: - return hwAnalog1Input + t, _ := InputAnalog1.(T) + return t case hal.HardwareIdDisplay1Output: - return hwDisplay1Output + t, _ := OutputDisplay1.(T) + return t case hal.HardwareIdButton1Input: - return hwButton1Input + t, _ := InputButton1.(T) + return t case hal.HardwareIdButton2Input: - return hwButton2Input + t, _ := InputButton2.(T) + return t case hal.HardwareIdKnob1Input: - return hwKnob1Input + t, _ := InputKnob1.(T) + return t case hal.HardwareIdKnob2Input: - return hwKnob2Input + t, _ := InputKnob2.(T) + return t case hal.HardwareIdVoltage1Output: - return hwCV1Output + t, _ := OutputVoltage1.(T) + return t case hal.HardwareIdVoltage2Output: - return hwCV2Output + t, _ := OutputVoltage2.(T) + return t case hal.HardwareIdVoltage3Output: - return hwCV3Output + t, _ := OutputVoltage3.(T) + return t case hal.HardwareIdVoltage4Output: - return hwCV4Output + t, _ := OutputVoltage4.(T) + return t case hal.HardwareIdVoltage5Output: - return hwCV5Output + t, _ := OutputVoltage5.(T) + return t case hal.HardwareIdVoltage6Output: - return hwCV6Output + t, _ := OutputVoltage6.(T) + return t case hal.HardwareIdRandom1Generator: - return hwRandom1Generator + t, _ := DeviceRandomGenerator1.(T) + return t default: - return nil + var none T + return none } } diff --git a/internal/hardware/rev1/revisionmarker.go b/internal/hardware/rev1/revisionmarker.go new file mode 100644 index 0000000..97c10a8 --- /dev/null +++ b/internal/hardware/rev1/revisionmarker.go @@ -0,0 +1,13 @@ +package rev1 + +import "github.com/heucuva/europi/internal/hardware/hal" + +type revisionMarker struct{} + +func newRevisionMarker() hal.RevisionMarker { + return &revisionMarker{} +} + +func (r *revisionMarker) Revision() hal.Revision { + return hal.Revision1 +} diff --git a/internal/hardware/revision.go b/internal/hardware/revision.go index de55060..37e7b70 100644 --- a/internal/hardware/revision.go +++ b/internal/hardware/revision.go @@ -1,11 +1,14 @@ package hardware -type Revision int +import "github.com/heucuva/europi/internal/hardware/hal" + +type Revision = hal.Revision const ( - Revision0 = Revision(iota) - Revision1 - Revision2 + RevisionUnknown = hal.RevisionUnknown + Revision0 = hal.Revision0 + Revision1 = hal.Revision1 + Revision2 = hal.Revision2 ) // aliases From 41dd7237d998e1243be0c26bf66560ddbb2c9d42 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 01:51:51 -0700 Subject: [PATCH 15/62] minimal diagnostics app --- internal/projects/diagnostics/diagnostics.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index 44dd3ca..6955ebd 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -27,11 +27,11 @@ func startLoop(e *europi.EuroPi) { myApp.staticCv = 5 // Demonstrate adding a IRQ handler to B1 and B2. - e.B1.Handler(func(value bool, deltaTime time.Duration) { + e.B1.Handler(func(_ bool, _ time.Duration) { myApp.knobsDisplayPercent = !myApp.knobsDisplayPercent }) - e.B2.Handler(func(value bool, deltaTime time.Duration) { + e.B2.Handler(func(_ bool, _ time.Duration) { myApp.staticCv = (myApp.staticCv + 1) % int(e.K1.MaxVoltage()) }) } @@ -40,7 +40,7 @@ var ( DefaultFont = &proggy.TinySZ8pt7b ) -func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { +func mainLoop(e *europi.EuroPi) { e.Display.ClearBuffer() // Highlight the border of the oled display. @@ -88,9 +88,10 @@ func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { } func main() { - europi.Bootstrap( - europi.StartLoop(startLoop), - europi.MainLoop(mainLoop), - europi.MainLoopInterval(time.Millisecond*1), - ) + e := europi.New() + startLoop(e) + for { + mainLoop(e) + time.Sleep(time.Millisecond) + } } From c254ba35ba368308587fd87f151757d34a24d29a Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 10:36:47 -0700 Subject: [PATCH 16/62] move some stuff around - add a little bit of testing --- bootstrap_features.go | 4 +- bootstrap_panic.go | 2 +- bootstrap_ui.go | 2 +- bootstrap_uimodule.go | 2 +- europi.go | 6 +- {internal/event => event}/bus.go | 5 + experimental/displaylogger/logger.go | 6 +- experimental/fontwriter/writer.go | 2 +- experimental/knobbank/knobbank.go | 6 +- experimental/knobbank/knobbankentry.go | 6 +- experimental/knobbank/knoboptions.go | 2 +- experimental/knobmenu/item.go | 2 +- experimental/knobmenu/knobmenu.go | 12 +- experimental/knobmenu/options.go | 4 +- experimental/quantizer/quantizer_round.go | 2 +- experimental/quantizer/quantizer_trunc.go | 2 +- experimental/screenbank/screenbank.go | 6 +- experimental/screenbank/screenbankentry.go | 2 +- experimental/screenbank/screenbankoptions.go | 2 +- go.mod | 2 +- {internal/hardware => hardware}/README.md | 0 {internal/hardware => hardware}/hal.go | 0 .../hardware => hardware}/hal/analoginput.go | 2 +- .../hardware => hardware}/hal/buttoninput.go | 0 .../hardware => hardware}/hal/changes.go | 0 .../hardware => hardware}/hal/digitalinput.go | 0 .../hal/displayoutput.go | 0 {internal/hardware => hardware}/hal/hal.go | 0 .../hardware => hardware}/hal/knobinput.go | 0 .../hal/randomgenerator.go | 0 .../hal/revisionmarker.go | 0 .../hal/voltageoutput.go | 2 +- {internal/hardware => hardware}/platform.go | 4 +- .../hardware => hardware}/rev1/analoginput.go | 8 +- .../rev1/digitalinput.go | 4 +- .../rev1/displayoutput.go | 2 +- .../hardware => hardware}/rev1/messages.go | 2 +- .../hardware => hardware}/rev1/nonpico.go | 4 +- {internal/hardware => hardware}/rev1/pico.go | 2 +- .../hardware => hardware}/rev1/platform.go | 2 +- .../rev1/randomgenerator.go | 2 +- .../rev1/revisionmarker.go | 2 +- .../rev1/voltageoutput.go | 6 +- {internal/hardware => hardware}/revision.go | 2 +- .../projects/clockgenerator/clockgenerator.go | 8 +- .../projects/clockgenerator/module/module.go | 2 +- .../clockgenerator/module/setting_bpm.go | 4 +- .../module/setting_gateduration.go | 4 +- .../projects/clockgenerator/screen/main.go | 8 +- .../clockgenerator/screen/settings.go | 8 +- internal/projects/clockwerk/clockwerk.go | 12 +- internal/projects/diagnostics/diagnostics.go | 6 +- .../projects/randomskips/module/module.go | 2 +- .../randomskips/module/setting_chance.go | 4 +- internal/projects/randomskips/randomskips.go | 14 +- internal/projects/randomskips/screen/main.go | 10 +- .../projects/randomskips/screen/settings.go | 8 +- lerp/lerp32.go | 2 +- lerp/lerp64.go | 2 +- units/bipolarcv.go | 17 +- units/bipolarcv_test.go | 163 ++++++++++++++++++ units/cv.go | 16 +- units/cv_test.go | 145 ++++++++++++++++ units/hertz.go | 24 ++- units/units_debug.go | 14 -- units/units_release.go | 7 - units/voct.go | 1 - 67 files changed, 456 insertions(+), 146 deletions(-) rename {internal/event => event}/bus.go (86%) rename {internal/hardware => hardware}/README.md (100%) rename {internal/hardware => hardware}/hal.go (100%) rename {internal/hardware => hardware}/hal/analoginput.go (88%) rename {internal/hardware => hardware}/hal/buttoninput.go (100%) rename {internal/hardware => hardware}/hal/changes.go (100%) rename {internal/hardware => hardware}/hal/digitalinput.go (100%) rename {internal/hardware => hardware}/hal/displayoutput.go (100%) rename {internal/hardware => hardware}/hal/hal.go (100%) rename {internal/hardware => hardware}/hal/knobinput.go (100%) rename {internal/hardware => hardware}/hal/randomgenerator.go (100%) rename {internal/hardware => hardware}/hal/revisionmarker.go (100%) rename {internal/hardware => hardware}/hal/voltageoutput.go (88%) rename {internal/hardware => hardware}/platform.go (86%) rename {internal/hardware => hardware}/rev1/analoginput.go (94%) rename {internal/hardware => hardware}/rev1/digitalinput.go (95%) rename {internal/hardware => hardware}/rev1/displayoutput.go (93%) rename {internal/hardware => hardware}/rev1/messages.go (87%) rename {internal/hardware => hardware}/rev1/nonpico.go (97%) rename {internal/hardware => hardware}/rev1/pico.go (99%) rename {internal/hardware => hardware}/rev1/platform.go (97%) rename {internal/hardware => hardware}/rev1/randomgenerator.go (89%) rename {internal/hardware => hardware}/rev1/revisionmarker.go (77%) rename {internal/hardware => hardware}/rev1/voltageoutput.go (94%) rename {internal/hardware => hardware}/revision.go (83%) create mode 100644 units/bipolarcv_test.go create mode 100644 units/cv_test.go delete mode 100644 units/units_debug.go delete mode 100644 units/units_release.go diff --git a/bootstrap_features.go b/bootstrap_features.go index ed3762c..31eb766 100644 --- a/bootstrap_features.go +++ b/bootstrap_features.go @@ -4,8 +4,8 @@ import ( "log" "os" - "github.com/heucuva/europi/experimental/displaylogger" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/experimental/displaylogger" + "github.com/awonak/EuroPiGo/hardware/hal" ) var ( diff --git a/bootstrap_panic.go b/bootstrap_panic.go index 699cbf1..5b06cf1 100644 --- a/bootstrap_panic.go +++ b/bootstrap_panic.go @@ -4,7 +4,7 @@ import ( "fmt" "log" - "github.com/heucuva/europi/experimental/draw" + "github.com/awonak/EuroPiGo/experimental/draw" "tinygo.org/x/tinydraw" ) diff --git a/bootstrap_ui.go b/bootstrap_ui.go index 988438d..f48452c 100644 --- a/bootstrap_ui.go +++ b/bootstrap_ui.go @@ -3,7 +3,7 @@ package europi import ( "time" - "github.com/heucuva/europi/debounce" + "github.com/awonak/EuroPiGo/debounce" ) type UserInterface interface { diff --git a/bootstrap_uimodule.go b/bootstrap_uimodule.go index 48db79f..2cedd09 100644 --- a/bootstrap_uimodule.go +++ b/bootstrap_uimodule.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/hal" ) type uiModule struct { diff --git a/europi.go b/europi.go index 3945d39..889a9ad 100644 --- a/europi.go +++ b/europi.go @@ -1,8 +1,8 @@ -package europi // import "github.com/heucuva/europi" +package europi // import "github.com/awonak/EuroPiGo" import ( - "github.com/heucuva/europi/internal/hardware" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/hardware" + "github.com/awonak/EuroPiGo/hardware/hal" ) // EuroPi is the collection of component wrappers used to interact with the module. diff --git a/internal/event/bus.go b/event/bus.go similarity index 86% rename from internal/event/bus.go rename to event/bus.go index 9bc5b37..4ce7f51 100644 --- a/internal/event/bus.go +++ b/event/bus.go @@ -4,6 +4,7 @@ import "sync" type Bus interface { Subscribe(subject string, callback func(msg any)) + Unsubscribe(subject string) Post(subject string, msg any) } @@ -28,6 +29,10 @@ 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 { diff --git a/experimental/displaylogger/logger.go b/experimental/displaylogger/logger.go index 43a642c..597a59a 100644 --- a/experimental/displaylogger/logger.go +++ b/experimental/displaylogger/logger.go @@ -4,9 +4,9 @@ import ( "io" "strings" - "github.com/heucuva/europi/experimental/draw" - "github.com/heucuva/europi/experimental/fontwriter" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/experimental/draw" + "github.com/awonak/EuroPiGo/experimental/fontwriter" + "github.com/awonak/EuroPiGo/hardware/hal" "tinygo.org/x/tinyfont/proggy" ) diff --git a/experimental/fontwriter/writer.go b/experimental/fontwriter/writer.go index 97b4331..7db82e0 100644 --- a/experimental/fontwriter/writer.go +++ b/experimental/fontwriter/writer.go @@ -3,7 +3,7 @@ package fontwriter import ( "image/color" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/hal" "tinygo.org/x/tinydraw" "tinygo.org/x/tinyfont" ) diff --git a/experimental/knobbank/knobbank.go b/experimental/knobbank/knobbank.go index 3ca1f21..9b97c1c 100644 --- a/experimental/knobbank/knobbank.go +++ b/experimental/knobbank/knobbank.go @@ -3,9 +3,9 @@ package knobbank import ( "errors" - "github.com/heucuva/europi/clamp" - "github.com/heucuva/europi/internal/hardware/hal" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/clamp" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/units" ) type KnobBank struct { diff --git a/experimental/knobbank/knobbankentry.go b/experimental/knobbank/knobbankentry.go index a2370f1..0fd7c3b 100644 --- a/experimental/knobbank/knobbankentry.go +++ b/experimental/knobbank/knobbankentry.go @@ -3,9 +3,9 @@ package knobbank import ( "math" - "github.com/heucuva/europi/clamp" - "github.com/heucuva/europi/internal/hardware/hal" - "github.com/heucuva/europi/lerp" + "github.com/awonak/EuroPiGo/clamp" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/lerp" ) type knobBankEntry struct { diff --git a/experimental/knobbank/knoboptions.go b/experimental/knobbank/knoboptions.go index eead862..b5e6142 100644 --- a/experimental/knobbank/knoboptions.go +++ b/experimental/knobbank/knoboptions.go @@ -3,7 +3,7 @@ package knobbank import ( "fmt" - "github.com/heucuva/europi/lerp" + "github.com/awonak/EuroPiGo/lerp" ) type KnobOption func(e *knobBankEntry) error diff --git a/experimental/knobmenu/item.go b/experimental/knobmenu/item.go index 2009e9b..0ce68d7 100644 --- a/experimental/knobmenu/item.go +++ b/experimental/knobmenu/item.go @@ -1,6 +1,6 @@ package knobmenu -import "github.com/heucuva/europi/units" +import "github.com/awonak/EuroPiGo/units" type item struct { name string diff --git a/experimental/knobmenu/knobmenu.go b/experimental/knobmenu/knobmenu.go index fa361db..1f40183 100644 --- a/experimental/knobmenu/knobmenu.go +++ b/experimental/knobmenu/knobmenu.go @@ -4,12 +4,12 @@ import ( "fmt" "time" - "github.com/heucuva/europi" - "github.com/heucuva/europi/clamp" - "github.com/heucuva/europi/experimental/draw" - "github.com/heucuva/europi/experimental/fontwriter" - "github.com/heucuva/europi/experimental/knobbank" - "github.com/heucuva/europi/internal/hardware/hal" + 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/experimental/knobbank" + "github.com/awonak/EuroPiGo/hardware/hal" "tinygo.org/x/tinyfont/proggy" ) diff --git a/experimental/knobmenu/options.go b/experimental/knobmenu/options.go index 679edbb..db55911 100644 --- a/experimental/knobmenu/options.go +++ b/experimental/knobmenu/options.go @@ -3,8 +3,8 @@ package knobmenu import ( "fmt" - "github.com/heucuva/europi/experimental/knobbank" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/experimental/knobbank" + "github.com/awonak/EuroPiGo/units" "tinygo.org/x/tinyfont" ) diff --git a/experimental/quantizer/quantizer_round.go b/experimental/quantizer/quantizer_round.go index 1b83651..f7a9395 100644 --- a/experimental/quantizer/quantizer_round.go +++ b/experimental/quantizer/quantizer_round.go @@ -1,7 +1,7 @@ package quantizer import ( - "github.com/heucuva/europi/lerp" + "github.com/awonak/EuroPiGo/lerp" ) type Round[T any] struct{} diff --git a/experimental/quantizer/quantizer_trunc.go b/experimental/quantizer/quantizer_trunc.go index 3759027..dd6d9c3 100644 --- a/experimental/quantizer/quantizer_trunc.go +++ b/experimental/quantizer/quantizer_trunc.go @@ -1,7 +1,7 @@ package quantizer import ( - "github.com/heucuva/europi/lerp" + "github.com/awonak/EuroPiGo/lerp" ) type Trunc[T any] struct{} diff --git a/experimental/screenbank/screenbank.go b/experimental/screenbank/screenbank.go index 854238e..1f33e7c 100644 --- a/experimental/screenbank/screenbank.go +++ b/experimental/screenbank/screenbank.go @@ -3,9 +3,9 @@ package screenbank import ( "time" - "github.com/heucuva/europi" - "github.com/heucuva/europi/experimental/draw" - "github.com/heucuva/europi/experimental/fontwriter" + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/draw" + "github.com/awonak/EuroPiGo/experimental/fontwriter" "tinygo.org/x/tinyfont/notoemoji" ) diff --git a/experimental/screenbank/screenbankentry.go b/experimental/screenbank/screenbankentry.go index 0d1d29c..e3fdf10 100644 --- a/experimental/screenbank/screenbankentry.go +++ b/experimental/screenbank/screenbankentry.go @@ -3,7 +3,7 @@ package screenbank import ( "time" - "github.com/heucuva/europi" + europi "github.com/awonak/EuroPiGo" ) type screenBankEntry struct { diff --git a/experimental/screenbank/screenbankoptions.go b/experimental/screenbank/screenbankoptions.go index 39a9dc8..22aefa3 100644 --- a/experimental/screenbank/screenbankoptions.go +++ b/experimental/screenbank/screenbankoptions.go @@ -3,7 +3,7 @@ package screenbank import ( "time" - "github.com/heucuva/europi" + europi "github.com/awonak/EuroPiGo" ) type ScreenBankOption func(sb *ScreenBank) error diff --git a/go.mod b/go.mod index a787bd8..86e8466 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/heucuva/europi +module github.com/awonak/EuroPiGo go 1.18 diff --git a/internal/hardware/README.md b/hardware/README.md similarity index 100% rename from internal/hardware/README.md rename to hardware/README.md diff --git a/internal/hardware/hal.go b/hardware/hal.go similarity index 100% rename from internal/hardware/hal.go rename to hardware/hal.go diff --git a/internal/hardware/hal/analoginput.go b/hardware/hal/analoginput.go similarity index 88% rename from internal/hardware/hal/analoginput.go rename to hardware/hal/analoginput.go index 94022a7..12ae33b 100644 --- a/internal/hardware/hal/analoginput.go +++ b/hardware/hal/analoginput.go @@ -1,6 +1,6 @@ package hal -import "github.com/heucuva/europi/units" +import "github.com/awonak/EuroPiGo/units" type AnalogInput interface { Configure(config AnalogInputConfig) error diff --git a/internal/hardware/hal/buttoninput.go b/hardware/hal/buttoninput.go similarity index 100% rename from internal/hardware/hal/buttoninput.go rename to hardware/hal/buttoninput.go diff --git a/internal/hardware/hal/changes.go b/hardware/hal/changes.go similarity index 100% rename from internal/hardware/hal/changes.go rename to hardware/hal/changes.go diff --git a/internal/hardware/hal/digitalinput.go b/hardware/hal/digitalinput.go similarity index 100% rename from internal/hardware/hal/digitalinput.go rename to hardware/hal/digitalinput.go diff --git a/internal/hardware/hal/displayoutput.go b/hardware/hal/displayoutput.go similarity index 100% rename from internal/hardware/hal/displayoutput.go rename to hardware/hal/displayoutput.go diff --git a/internal/hardware/hal/hal.go b/hardware/hal/hal.go similarity index 100% rename from internal/hardware/hal/hal.go rename to hardware/hal/hal.go diff --git a/internal/hardware/hal/knobinput.go b/hardware/hal/knobinput.go similarity index 100% rename from internal/hardware/hal/knobinput.go rename to hardware/hal/knobinput.go diff --git a/internal/hardware/hal/randomgenerator.go b/hardware/hal/randomgenerator.go similarity index 100% rename from internal/hardware/hal/randomgenerator.go rename to hardware/hal/randomgenerator.go diff --git a/internal/hardware/hal/revisionmarker.go b/hardware/hal/revisionmarker.go similarity index 100% rename from internal/hardware/hal/revisionmarker.go rename to hardware/hal/revisionmarker.go diff --git a/internal/hardware/hal/voltageoutput.go b/hardware/hal/voltageoutput.go similarity index 88% rename from internal/hardware/hal/voltageoutput.go rename to hardware/hal/voltageoutput.go index 7029137..1407619 100644 --- a/internal/hardware/hal/voltageoutput.go +++ b/hardware/hal/voltageoutput.go @@ -3,7 +3,7 @@ package hal import ( "time" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/units" ) type VoltageOutput interface { diff --git a/internal/hardware/platform.go b/hardware/platform.go similarity index 86% rename from internal/hardware/platform.go rename to hardware/platform.go index a7f8724..2c175f5 100644 --- a/internal/hardware/platform.go +++ b/hardware/platform.go @@ -1,8 +1,8 @@ package hardware import ( - "github.com/heucuva/europi/internal/hardware/hal" - "github.com/heucuva/europi/internal/hardware/rev1" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" ) func GetHardware[T any](revision Revision, id hal.HardwareId) T { diff --git a/internal/hardware/rev1/analoginput.go b/hardware/rev1/analoginput.go similarity index 94% rename from internal/hardware/rev1/analoginput.go rename to hardware/rev1/analoginput.go index 7f11712..8b7bb41 100644 --- a/internal/hardware/rev1/analoginput.go +++ b/hardware/rev1/analoginput.go @@ -3,10 +3,10 @@ package rev1 import ( "errors" - "github.com/heucuva/europi/clamp" - "github.com/heucuva/europi/internal/hardware/hal" - "github.com/heucuva/europi/lerp" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/clamp" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/lerp" + "github.com/awonak/EuroPiGo/units" ) const ( diff --git a/internal/hardware/rev1/digitalinput.go b/hardware/rev1/digitalinput.go similarity index 95% rename from internal/hardware/rev1/digitalinput.go rename to hardware/rev1/digitalinput.go index 9923184..fc47073 100644 --- a/internal/hardware/rev1/digitalinput.go +++ b/hardware/rev1/digitalinput.go @@ -3,8 +3,8 @@ package rev1 import ( "time" - "github.com/heucuva/europi/debounce" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/debounce" + "github.com/awonak/EuroPiGo/hardware/hal" ) // digitalinput is a struct for handling reading of the digital input. diff --git a/internal/hardware/rev1/displayoutput.go b/hardware/rev1/displayoutput.go similarity index 93% rename from internal/hardware/rev1/displayoutput.go rename to hardware/rev1/displayoutput.go index 188286b..2f3b198 100644 --- a/internal/hardware/rev1/displayoutput.go +++ b/hardware/rev1/displayoutput.go @@ -3,7 +3,7 @@ package rev1 import ( "image/color" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/hal" ) // displayoutput is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED. diff --git a/internal/hardware/rev1/messages.go b/hardware/rev1/messages.go similarity index 87% rename from internal/hardware/rev1/messages.go rename to hardware/rev1/messages.go index ede1024..a8fe0c0 100644 --- a/internal/hardware/rev1/messages.go +++ b/hardware/rev1/messages.go @@ -1,6 +1,6 @@ package rev1 -import "github.com/heucuva/europi/internal/hardware/hal" +import "github.com/awonak/EuroPiGo/hardware/hal" type HwMessageDigitalValue struct { Value bool diff --git a/internal/hardware/rev1/nonpico.go b/hardware/rev1/nonpico.go similarity index 97% rename from internal/hardware/rev1/nonpico.go rename to hardware/rev1/nonpico.go index c520e48..e4cc87b 100644 --- a/internal/hardware/rev1/nonpico.go +++ b/hardware/rev1/nonpico.go @@ -8,8 +8,8 @@ import ( "image/color" "math" - "github.com/heucuva/europi/internal/event" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/hal" ) var ( diff --git a/internal/hardware/rev1/pico.go b/hardware/rev1/pico.go similarity index 99% rename from internal/hardware/rev1/pico.go rename to hardware/rev1/pico.go index 6560ddd..2edc749 100644 --- a/internal/hardware/rev1/pico.go +++ b/hardware/rev1/pico.go @@ -12,7 +12,7 @@ import ( "runtime/interrupt" "runtime/volatile" - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/hal" "tinygo.org/x/drivers/ssd1306" ) diff --git a/internal/hardware/rev1/platform.go b/hardware/rev1/platform.go similarity index 97% rename from internal/hardware/rev1/platform.go rename to hardware/rev1/platform.go index 4d1af8a..9800a3d 100644 --- a/internal/hardware/rev1/platform.go +++ b/hardware/rev1/platform.go @@ -1,7 +1,7 @@ package rev1 import ( - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/hal" ) var ( diff --git a/internal/hardware/rev1/randomgenerator.go b/hardware/rev1/randomgenerator.go similarity index 89% rename from internal/hardware/rev1/randomgenerator.go rename to hardware/rev1/randomgenerator.go index bb7b127..0ed6f06 100644 --- a/internal/hardware/rev1/randomgenerator.go +++ b/hardware/rev1/randomgenerator.go @@ -1,7 +1,7 @@ package rev1 import ( - "github.com/heucuva/europi/internal/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/hal" ) type randomGenerator struct { diff --git a/internal/hardware/rev1/revisionmarker.go b/hardware/rev1/revisionmarker.go similarity index 77% rename from internal/hardware/rev1/revisionmarker.go rename to hardware/rev1/revisionmarker.go index 97c10a8..50abd06 100644 --- a/internal/hardware/rev1/revisionmarker.go +++ b/hardware/rev1/revisionmarker.go @@ -1,6 +1,6 @@ package rev1 -import "github.com/heucuva/europi/internal/hardware/hal" +import "github.com/awonak/EuroPiGo/hardware/hal" type revisionMarker struct{} diff --git a/internal/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go similarity index 94% rename from internal/hardware/rev1/voltageoutput.go rename to hardware/rev1/voltageoutput.go index 4ec15ad..3bd028b 100644 --- a/internal/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -4,9 +4,9 @@ import ( "fmt" "time" - "github.com/heucuva/europi/clamp" - "github.com/heucuva/europi/internal/hardware/hal" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/clamp" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/units" ) const ( diff --git a/internal/hardware/revision.go b/hardware/revision.go similarity index 83% rename from internal/hardware/revision.go rename to hardware/revision.go index 37e7b70..8b1939c 100644 --- a/internal/hardware/revision.go +++ b/hardware/revision.go @@ -1,6 +1,6 @@ package hardware -import "github.com/heucuva/europi/internal/hardware/hal" +import "github.com/awonak/EuroPiGo/hardware/hal" type Revision = hal.Revision diff --git a/internal/projects/clockgenerator/clockgenerator.go b/internal/projects/clockgenerator/clockgenerator.go index 30c475d..0df3d9a 100644 --- a/internal/projects/clockgenerator/clockgenerator.go +++ b/internal/projects/clockgenerator/clockgenerator.go @@ -3,10 +3,10 @@ package main import ( "time" - "github.com/heucuva/europi" - "github.com/heucuva/europi/experimental/screenbank" - "github.com/heucuva/europi/internal/projects/clockgenerator/module" - "github.com/heucuva/europi/internal/projects/clockgenerator/screen" + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/screenbank" + "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" + "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" ) var ( diff --git a/internal/projects/clockgenerator/module/module.go b/internal/projects/clockgenerator/module/module.go index 3b8146f..b4935d3 100644 --- a/internal/projects/clockgenerator/module/module.go +++ b/internal/projects/clockgenerator/module/module.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/heucuva/europi/clamp" + "github.com/awonak/EuroPiGo/clamp" ) type ClockGenerator struct { diff --git a/internal/projects/clockgenerator/module/setting_bpm.go b/internal/projects/clockgenerator/module/setting_bpm.go index 10e7dc7..6ad146c 100644 --- a/internal/projects/clockgenerator/module/setting_bpm.go +++ b/internal/projects/clockgenerator/module/setting_bpm.go @@ -3,8 +3,8 @@ package module import ( "fmt" - "github.com/heucuva/europi/lerp" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/lerp" + "github.com/awonak/EuroPiGo/units" ) const ( diff --git a/internal/projects/clockgenerator/module/setting_gateduration.go b/internal/projects/clockgenerator/module/setting_gateduration.go index db76265..79f90bb 100644 --- a/internal/projects/clockgenerator/module/setting_gateduration.go +++ b/internal/projects/clockgenerator/module/setting_gateduration.go @@ -3,8 +3,8 @@ package module import ( "time" - "github.com/heucuva/europi/lerp" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/lerp" + "github.com/awonak/EuroPiGo/units" ) const ( diff --git a/internal/projects/clockgenerator/screen/main.go b/internal/projects/clockgenerator/screen/main.go index 7eaca6c..3d102c8 100644 --- a/internal/projects/clockgenerator/screen/main.go +++ b/internal/projects/clockgenerator/screen/main.go @@ -4,10 +4,10 @@ import ( "fmt" "time" - "github.com/heucuva/europi" - "github.com/heucuva/europi/experimental/draw" - "github.com/heucuva/europi/experimental/fontwriter" - "github.com/heucuva/europi/internal/projects/clockgenerator/module" + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/draw" + "github.com/awonak/EuroPiGo/experimental/fontwriter" + "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" "tinygo.org/x/tinydraw" "tinygo.org/x/tinyfont/proggy" ) diff --git a/internal/projects/clockgenerator/screen/settings.go b/internal/projects/clockgenerator/screen/settings.go index 7c53d98..2722ef2 100644 --- a/internal/projects/clockgenerator/screen/settings.go +++ b/internal/projects/clockgenerator/screen/settings.go @@ -3,10 +3,10 @@ package screen import ( "time" - "github.com/heucuva/europi" - "github.com/heucuva/europi/experimental/knobmenu" - "github.com/heucuva/europi/internal/projects/clockgenerator/module" - "github.com/heucuva/europi/units" + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/knobmenu" + "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" + "github.com/awonak/EuroPiGo/units" ) type Settings struct { diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index 0178b4f..6d6f993 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -34,12 +34,12 @@ import ( "tinygo.org/x/tinydraw" "tinygo.org/x/tinyfont/proggy" - "github.com/heucuva/europi" - "github.com/heucuva/europi/clamp" - "github.com/heucuva/europi/experimental/draw" - "github.com/heucuva/europi/experimental/fontwriter" - "github.com/heucuva/europi/internal/hardware/hal" - "github.com/heucuva/europi/lerp" + 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 ( diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index 6955ebd..7b9d1c4 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -8,9 +8,9 @@ import ( "tinygo.org/x/tinydraw" "tinygo.org/x/tinyfont/proggy" - "github.com/heucuva/europi" - "github.com/heucuva/europi/experimental/draw" - "github.com/heucuva/europi/experimental/fontwriter" + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/draw" + "github.com/awonak/EuroPiGo/experimental/fontwriter" ) type MyApp struct { diff --git a/internal/projects/randomskips/module/module.go b/internal/projects/randomskips/module/module.go index b7853c1..b4fc2ca 100644 --- a/internal/projects/randomskips/module/module.go +++ b/internal/projects/randomskips/module/module.go @@ -4,7 +4,7 @@ import ( "math/rand" "time" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/units" ) type RandomSkips struct { diff --git a/internal/projects/randomskips/module/setting_chance.go b/internal/projects/randomskips/module/setting_chance.go index c7a83b7..74a95fc 100644 --- a/internal/projects/randomskips/module/setting_chance.go +++ b/internal/projects/randomskips/module/setting_chance.go @@ -3,8 +3,8 @@ package module import ( "fmt" - "github.com/heucuva/europi/clamp" - "github.com/heucuva/europi/units" + "github.com/awonak/EuroPiGo/clamp" + "github.com/awonak/EuroPiGo/units" ) func ChanceString(chance float32) string { diff --git a/internal/projects/randomskips/randomskips.go b/internal/projects/randomskips/randomskips.go index 76d60ef..377cc9e 100644 --- a/internal/projects/randomskips/randomskips.go +++ b/internal/projects/randomskips/randomskips.go @@ -3,13 +3,13 @@ package main import ( "time" - "github.com/heucuva/europi" - "github.com/heucuva/europi/experimental/screenbank" - "github.com/heucuva/europi/internal/hardware/hal" - clockgenerator "github.com/heucuva/europi/internal/projects/clockgenerator/module" - clockScreen "github.com/heucuva/europi/internal/projects/clockgenerator/screen" - "github.com/heucuva/europi/internal/projects/randomskips/module" - "github.com/heucuva/europi/internal/projects/randomskips/screen" + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/screenbank" + "github.com/awonak/EuroPiGo/hardware/hal" + clockgenerator "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" + clockScreen "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" + "github.com/awonak/EuroPiGo/internal/projects/randomskips/module" + "github.com/awonak/EuroPiGo/internal/projects/randomskips/screen" ) var ( diff --git a/internal/projects/randomskips/screen/main.go b/internal/projects/randomskips/screen/main.go index e6205aa..87eaabb 100644 --- a/internal/projects/randomskips/screen/main.go +++ b/internal/projects/randomskips/screen/main.go @@ -4,11 +4,11 @@ import ( "fmt" "time" - "github.com/heucuva/europi" - "github.com/heucuva/europi/experimental/draw" - "github.com/heucuva/europi/experimental/fontwriter" - clockgenerator "github.com/heucuva/europi/internal/projects/clockgenerator/module" - "github.com/heucuva/europi/internal/projects/randomskips/module" + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/draw" + "github.com/awonak/EuroPiGo/experimental/fontwriter" + clockgenerator "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" + "github.com/awonak/EuroPiGo/internal/projects/randomskips/module" "tinygo.org/x/tinydraw" "tinygo.org/x/tinyfont/proggy" ) diff --git a/internal/projects/randomskips/screen/settings.go b/internal/projects/randomskips/screen/settings.go index 25c31da..0884b24 100644 --- a/internal/projects/randomskips/screen/settings.go +++ b/internal/projects/randomskips/screen/settings.go @@ -3,10 +3,10 @@ package screen import ( "time" - "github.com/heucuva/europi" - "github.com/heucuva/europi/experimental/knobmenu" - "github.com/heucuva/europi/internal/projects/randomskips/module" - "github.com/heucuva/europi/units" + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/knobmenu" + "github.com/awonak/EuroPiGo/internal/projects/randomskips/module" + "github.com/awonak/EuroPiGo/units" ) type Settings struct { diff --git a/lerp/lerp32.go b/lerp/lerp32.go index 1c0fcb7..fbfe4db 100644 --- a/lerp/lerp32.go +++ b/lerp/lerp32.go @@ -1,6 +1,6 @@ package lerp -import "github.com/heucuva/europi/clamp" +import "github.com/awonak/EuroPiGo/clamp" type lerp32[T Lerpable] struct { b T diff --git a/lerp/lerp64.go b/lerp/lerp64.go index 1791ac1..f3a6166 100644 --- a/lerp/lerp64.go +++ b/lerp/lerp64.go @@ -1,6 +1,6 @@ package lerp -import "github.com/heucuva/europi/clamp" +import "github.com/awonak/EuroPiGo/clamp" type lerp64[T Lerpable] struct { b T diff --git a/units/bipolarcv.go b/units/bipolarcv.go index f22fce3..8cde411 100644 --- a/units/bipolarcv.go +++ b/units/bipolarcv.go @@ -1,21 +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 { - v := float32(c) - range_check(v, -1, 1, "bipolarcv") - return v * 5 + return c.ToFloat32() * 5.0 } -// ToCV converts a (normalized) BipolarCV value to a (normalized) CV value -func (c BipolarCV) ToCV() CV { - return CV((c.ToFloat32() + 1.0) * 0.5) +// 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 float32(c) + 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..1123103 --- /dev/null +++ b/units/bipolarcv_test.go @@ -0,0 +1,163 @@ +package units_test + +import ( + "math" + "testing" + + "github.com/awonak/EuroPiGo/units" +) + +func TestBipolarCVToVolts(t *testing.T) { + t.Run("InRange", 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) + } + + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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 index 7a374da..b23c687 100644 --- a/units/cv.go +++ b/units/cv.go @@ -1,21 +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 { - v := float32(c) - range_check(v, 0, 1, "cv") - return v * 5 + return c.ToFloat32() * 5.0 } // ToBipolarCV converts a (normalized) CV value to a (normalized) BipolarCV value -func (c CV) ToBipolarCV() BipolarCV { - return BipolarCV(c.ToFloat32()*2.0 - 1.0) +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 float32(c) + 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..2b3851f --- /dev/null +++ b/units/cv_test.go @@ -0,0 +1,145 @@ +package units_test + +import ( + "math" + "testing" + + "github.com/awonak/EuroPiGo/units" +) + +func TestCVToVolts(t *testing.T) { + t.Run("InRange", func(t *testing.T) { + min := units.CV(0.0) + if expected, actual := float32(0.0), min.ToVolts(); actual != expected { + t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", min, expected, actual) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) { + min, minSign := units.CV(1.0), -1 + if actual, expected := min.ToBipolarCV(minSign), units.BipolarCV(-1.0); actual != expected { + t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", min, minSign, expected, actual) + } + + zero, zeroSign := units.CV(0.0), 1 + if actual, expected := zero.ToBipolarCV(zeroSign), units.BipolarCV(0.0); actual != expected { + t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", zero, zeroSign, expected, actual) + } + + max, maxSign := units.CV(1.0), 1 + if actual, expected := max.ToBipolarCV(maxSign), units.BipolarCV(1.0); actual != expected { + t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", max, maxSign, expected, actual) + } + }) + t.Run("OutOfRange", func(t *testing.T) { + belowMin, belowMinSign := units.CV(2.0), -1 + if actual, expected := belowMin.ToBipolarCV(belowMinSign), units.BipolarCV(-1.0); actual != expected { + t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", belowMin, belowMinSign, expected, actual) + } + + aboveMax, aboveMaxSign := units.CV(2.0), 1 + if actual, expected := aboveMax.ToBipolarCV(aboveMaxSign), units.BipolarCV(1.0); 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, math.NaN(), actual) + } + }) + + t.Run("Inf", func(t *testing.T) { + negInf, negInfSign := units.CV(math.Inf(1)), -1 + if actual, expected := negInf.ToBipolarCV(negInfSign), units.BipolarCV(-1.0); actual != expected { + t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", negInf, negInfSign, expected, actual) + } + + posInf, posInfSign := units.CV(math.Inf(1)), 1 + if actual, expected := posInf.ToBipolarCV(posInfSign), units.BipolarCV(1.0); 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) { + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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/hertz.go b/units/hertz.go index d9f9168..182ac39 100644 --- a/units/hertz.go +++ b/units/hertz.go @@ -8,18 +8,30 @@ import ( 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*1000000.0) - case h < 1: - return fmt.Sprintf("%3.1fmHz", h*1000.0) - case h >= 1000: - return fmt.Sprintf("%3.1fkHz", h/1000.0) + 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: - return fmt.Sprintf("%5.1fHz", h) + // use scientific notation + return fmt.Sprintf("%3.1gHz", h) } } diff --git a/units/units_debug.go b/units/units_debug.go deleted file mode 100644 index 907d850..0000000 --- a/units/units_debug.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build debug -// +build debug - -package units - -import ( - "fmt" -) - -func range_check[T ~float32 | ~float64](v, min, max T, kind string) { - if v < min || v > max { - panic(fmt.Errorf("%w: %v", fmt.Errorf("%s out of range", kind), v)) - } -} diff --git a/units/units_release.go b/units/units_release.go deleted file mode 100644 index 0ddb37f..0000000 --- a/units/units_release.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !debug -// +build !debug - -package units - -func range_check[T ~float32 | ~float64](v, min, max T, kind string) { -} diff --git a/units/voct.go b/units/voct.go index d2bfc56..f03dac3 100644 --- a/units/voct.go +++ b/units/voct.go @@ -11,7 +11,6 @@ type VOct float32 // ToVolts converts a V/Octave value to a value between 0.0 and 10.0 volts func (v VOct) ToVolts() float32 { voct := float32(v) - range_check(voct, 0, 10, "v/oct") return voct } From 3aecacc377f8f511df2dd8dc638e85b5db632387 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 12:51:27 -0700 Subject: [PATCH 17/62] Full code coverage for units --- units/bipolarcv_test.go | 248 +++++++++++++++++++++++----------------- units/duration.go | 8 +- units/duration_test.go | 52 +++++++++ units/hertz_test.go | 103 +++++++++++++++++ units/voct.go | 7 +- units/voct_test.go | 96 ++++++++++++++++ 6 files changed, 405 insertions(+), 109 deletions(-) create mode 100644 units/duration_test.go create mode 100644 units/hertz_test.go create mode 100644 units/voct_test.go diff --git a/units/bipolarcv_test.go b/units/bipolarcv_test.go index 1123103..67813c8 100644 --- a/units/bipolarcv_test.go +++ b/units/bipolarcv_test.go @@ -9,31 +9,41 @@ import ( func TestBipolarCVToVolts(t *testing.T) { t.Run("InRange", 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) - } - - 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) - } - - 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("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) { - 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) - } - - 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("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) { @@ -44,50 +54,64 @@ func TestBipolarCVToVolts(t *testing.T) { }) t.Run("Inf", 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) - } - - 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) - } + 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) { - 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) - } - - 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) - } - - 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("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) { - 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) - } - - 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("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) { @@ -99,47 +123,61 @@ func TestBipolarCVToCV(t *testing.T) { }) t.Run("Inf", 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) - } - - 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) - } + 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) { - 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) - } - - 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) - } - - 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("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) { - 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) - } - - 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("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) { @@ -150,14 +188,18 @@ func TestBipolarCVToFloat32(t *testing.T) { }) t.Run("Inf", 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) - } - - 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) - } + 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/duration.go b/units/duration.go index aa567be..8520690 100644 --- a/units/duration.go +++ b/units/duration.go @@ -7,11 +7,13 @@ import ( 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()*1000000.0) + return fmt.Sprintf("%3.1fus", dur.Seconds()*1_000_000.0) case dur < time.Second: - return fmt.Sprintf("%3.1fms", dur.Seconds()*1000.0) + return fmt.Sprintf("%3.1fms", dur.Seconds()*1_000.0) default: - return fmt.Sprint(dur) + 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_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 index f03dac3..c6a1a6b 100644 --- a/units/voct.go +++ b/units/voct.go @@ -1,5 +1,7 @@ package units +import "github.com/awonak/EuroPiGo/clamp" + const ( MinVOct VOct = 0.0 MaxVOct VOct = 10.0 @@ -10,11 +12,10 @@ type VOct float32 // ToVolts converts a V/Octave value to a value between 0.0 and 10.0 volts func (v VOct) ToVolts() float32 { - voct := float32(v) - return voct + 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(v) + 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..1b63fbc --- /dev/null +++ b/units/voct_test.go @@ -0,0 +1,96 @@ +package units_test + +import ( + "math" + "testing" + + "github.com/awonak/EuroPiGo/units" +) + +func TestVOctToVolts(t *testing.T) { + t.Run("InRange", 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) { + 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) + } + + 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) + } + }) +} From 09af866ab1cb6acde397183c7a190684923ebcac Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 13:38:11 -0700 Subject: [PATCH 18/62] add lerp - fix up units --- lerp/lerp32_test.go | 264 ++++++++++++++++++++++++++++++++++++++++++++ lerp/lerp64_test.go | 264 ++++++++++++++++++++++++++++++++++++++++++++ units/cv_test.go | 214 ++++++++++++++++++++--------------- units/voct_test.go | 120 ++++++++++++-------- 4 files changed, 727 insertions(+), 135 deletions(-) create mode 100644 lerp/lerp32_test.go create mode 100644 lerp/lerp64_test.go diff --git a/lerp/lerp32_test.go b/lerp/lerp32_test.go new file mode 100644 index 0000000..4b60c0e --- /dev/null +++ b/lerp/lerp32_test.go @@ -0,0 +1,264 @@ +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) + } + }) + }) + }) +} diff --git a/lerp/lerp64_test.go b/lerp/lerp64_test.go new file mode 100644 index 0000000..109d092 --- /dev/null +++ b/lerp/lerp64_test.go @@ -0,0 +1,264 @@ +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) + } + }) + }) + }) +} diff --git a/units/cv_test.go b/units/cv_test.go index 2b3851f..6ed525e 100644 --- a/units/cv_test.go +++ b/units/cv_test.go @@ -9,26 +9,35 @@ import ( func TestCVToVolts(t *testing.T) { t.Run("InRange", func(t *testing.T) { - min := units.CV(0.0) - if expected, actual := float32(0.0), min.ToVolts(); actual != expected { - t.Fatalf("CV[%v] ToVolts: expected[%f] actual[%f]", min, expected, actual) - } - - 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("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) { - 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) - } - 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("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) { @@ -39,89 +48,116 @@ func TestCVToVolts(t *testing.T) { }) t.Run("Inf", 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) - } - - 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) - } + 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) { - min, minSign := units.CV(1.0), -1 - if actual, expected := min.ToBipolarCV(minSign), units.BipolarCV(-1.0); actual != expected { - t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", min, minSign, expected, actual) - } - - zero, zeroSign := units.CV(0.0), 1 - if actual, expected := zero.ToBipolarCV(zeroSign), units.BipolarCV(0.0); actual != expected { - t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", zero, zeroSign, expected, actual) - } - - max, maxSign := units.CV(1.0), 1 - if actual, expected := max.ToBipolarCV(maxSign), units.BipolarCV(1.0); actual != expected { - t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", max, maxSign, expected, actual) - } + 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) { - belowMin, belowMinSign := units.CV(2.0), -1 - if actual, expected := belowMin.ToBipolarCV(belowMinSign), units.BipolarCV(-1.0); actual != expected { - t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", belowMin, belowMinSign, expected, actual) - } - aboveMax, aboveMaxSign := units.CV(2.0), 1 - if actual, expected := aboveMax.ToBipolarCV(aboveMaxSign), units.BipolarCV(1.0); actual != expected { - t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", aboveMax, aboveMaxSign, 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, math.NaN(), actual) + t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", nan, nanSign, nan, actual) } }) t.Run("Inf", func(t *testing.T) { - negInf, negInfSign := units.CV(math.Inf(1)), -1 - if actual, expected := negInf.ToBipolarCV(negInfSign), units.BipolarCV(-1.0); actual != expected { - t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", negInf, negInfSign, expected, actual) - } - - posInf, posInfSign := units.CV(math.Inf(1)), 1 - if actual, expected := posInf.ToBipolarCV(posInfSign), units.BipolarCV(1.0); actual != expected { - t.Fatalf("CV[%v sign(%d)] ToBipolarCV: expected[%v] actual[%v]", posInf, posInfSign, expected, actual) - } + 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) { - 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) - } - - 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("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) { - 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) - } - - 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("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) { @@ -132,14 +168,18 @@ func TestCVToFloat32(t *testing.T) { }) t.Run("Inf", 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) - } - - 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) - } + 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/voct_test.go b/units/voct_test.go index 1b63fbc..35f474d 100644 --- a/units/voct_test.go +++ b/units/voct_test.go @@ -9,26 +9,34 @@ import ( func TestVOctToVolts(t *testing.T) { t.Run("InRange", 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("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) + } + }) - 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("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) { - 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("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) + } + }) - 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("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) { @@ -39,40 +47,52 @@ func TestVOctToVolts(t *testing.T) { }) t.Run("Inf", 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("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) + } + }) - 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) - } + 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) { - 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("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) + } + }) - 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("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) { - 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("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) + } + }) - 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("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) { @@ -83,14 +103,18 @@ func TestVOctToFloat32(t *testing.T) { }) t.Run("Inf", 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("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) + } + }) - 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) - } + 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) + } + }) }) } From 21d5c379cf14bfe25caeaa4e350844c69170487e Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 13:44:42 -0700 Subject: [PATCH 19/62] add clamp tests --- clamp/clamp_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 clamp/clamp_test.go 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) + } + }) + }) +} From bf4490ca4aa96c65af0a576866eda091bf8dcb90 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 14:23:00 -0700 Subject: [PATCH 20/62] debounce tests --- debounce/debounce_test.go | 86 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 debounce/debounce_test.go diff --git a/debounce/debounce_test.go b/debounce/debounce_test.go new file mode 100644 index 0000000..beba825 --- /dev/null +++ b/debounce/debounce_test.go @@ -0,0 +1,86 @@ +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) { + 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) { + 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) { + 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) + } + }) +} From 9a69dbbf0de71cdf9c34347dd2b40d8330f2868f Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 16:43:51 -0700 Subject: [PATCH 21/62] Add parallel tests to debounce --- debounce/debounce_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/debounce/debounce_test.go b/debounce/debounce_test.go index beba825..38311ae 100644 --- a/debounce/debounce_test.go +++ b/debounce/debounce_test.go @@ -18,6 +18,7 @@ func TestDebouncer(t *testing.T) { 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) @@ -26,6 +27,7 @@ func TestDebouncer(t *testing.T) { }) t.Run("Delay100ms", func(t *testing.T) { + t.Parallel() delay := time.Millisecond * 100 runs := 4 runDebouncerTest(t, runs, delay, time.Duration(0), 0) @@ -34,6 +36,7 @@ func TestDebouncer(t *testing.T) { }) t.Run("Delay10s", func(t *testing.T) { + t.Parallel() delay := time.Millisecond * 100 runs := 4 runDebouncerTest(t, runs, delay, time.Duration(0), 0) From ffecaae939e8f0fa7aeff1697ca4b7b3dc6ea1b9 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 17:08:34 -0700 Subject: [PATCH 22/62] event bus testing and cautionary tale --- event/README.md | 15 +++++ event/bus_test.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 event/README.md create mode 100644 event/bus_test.go 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_test.go b/event/bus_test.go new file mode 100644 index 0000000..9edffe0 --- /dev/null +++ b/event/bus_test.go @@ -0,0 +1,136 @@ +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) + } + }) + }) +} + +/* +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) + } +} +*/ From 3833e059cd1611ba0c7fda357a5b148355fa53db Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 18:18:45 -0700 Subject: [PATCH 23/62] move hal code around to make more sense. - document revision and hardware ids - clean up linting of hardware/rev1 --- europi.go | 4 +-- hardware/README.md | 37 +++++++++++++++++++++++++++- hardware/hal.go | 26 ------------------- hardware/hal/displayoutput.go | 3 +++ hardware/hal/{hal.go => hardware.go} | 9 ++++++- hardware/hal/revision.go | 19 ++++++++++++++ hardware/hal/revisionmarker.go | 9 ------- hardware/platform.go | 16 +++++++----- hardware/rev1/README.md | 3 +++ hardware/rev1/analoginput.go | 12 +++++++-- hardware/rev1/digitalinput.go | 8 ++++++ hardware/rev1/displayoutput.go | 18 ++++++++++++++ hardware/rev1/messages.go | 6 +++++ hardware/rev1/platform.go | 4 +++ hardware/rev1/randomgenerator.go | 8 ++++++ hardware/rev1/revisionmarker.go | 8 ++++++ hardware/rev1/voltageoutput.go | 22 ++++++++++++----- hardware/revision.go | 19 -------------- 18 files changed, 159 insertions(+), 72 deletions(-) delete mode 100644 hardware/hal.go rename hardware/hal/{hal.go => hardware.go} (53%) create mode 100644 hardware/hal/revision.go create mode 100644 hardware/rev1/README.md delete mode 100644 hardware/revision.go diff --git a/europi.go b/europi.go index 889a9ad..6f3c414 100644 --- a/europi.go +++ b/europi.go @@ -25,8 +25,8 @@ type EuroPi struct { } // New will return a new EuroPi struct. -func New(opts ...hardware.Revision) *EuroPi { - revision := hardware.EuroPi +func New(opts ...hal.Revision) *EuroPi { + revision := hal.EuroPi if len(opts) > 0 { revision = opts[0] } diff --git a/hardware/README.md b/hardware/README.md index 5059dc2..4814f94 100644 --- a/hardware/README.md +++ b/hardware/README.md @@ -1,3 +1,38 @@ # hardware -This package is used for the [Original EuroPi hardware](https://github.com/Allen-Synthesis/EuroPi/tree/main/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 | 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` | EuroPi 'production' release, revision 1. | +| `Revision2` | `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. + +| Identifier | Alias | Interface | 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` | The Digital Input of the EuroPi. | +| `HardwareIdAnalog1Input` | `HardwareIdAnalogue1Input` | `hal.AnalogInput` | The Analogue Input of the EuroPi. | +| `HardwareIdDisplay1Output` | | `hal.DisplayOutput` | 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` | The Button 1 gate input of the EuroPi. | +| `HardwareIdButton2Input` | | `hal.ButtonInput` | The Button 2 gate input of the EuroPi. | +| `HardwareIdKnob1Input` | | `hal.KnobInput` | The Knob 1 potentiometer input of the EuroPi. | +| `HardwareIdKnob2Input` | | `hal.KnobInput` | The Knob 2 potentiometer input of the EuroPi. | +| `HardwareIdVoltage1Output` | `HardwareIdCV1Output` | `hal.VoltageOutput` | The #1 `CV` / `V/Octave` output of the EuroPi. While it 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. | +| `HardwareIdVoltage2Output` | `HardwareIdCV2Output` | `hal.VoltageOutput` | The #2 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | +| `HardwareIdVoltage3Output` | `HardwareIdCV3Output` | `hal.VoltageOutput` | The #3 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | +| `HardwareIdVoltage4Output` | `HardwareIdCV4Output` | `hal.VoltageOutput` | The #4 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | +| `HardwareIdVoltage5Output` | `HardwareIdCV5Output` | `hal.VoltageOutput` | The #5 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | +| `HardwareIdVoltage6Output` | `HardwareIdCV6Output` | `hal.VoltageOutput` | The #6 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | +| `HardwareIdRandom1Generator` | | `hal.RandomGenerator` | Provides an interface to calibrate or seed the random number generator of the hardware. | diff --git a/hardware/hal.go b/hardware/hal.go deleted file mode 100644 index 7f3ce76..0000000 --- a/hardware/hal.go +++ /dev/null @@ -1,26 +0,0 @@ -package hardware - -type HardwareId int - -const ( - HardwareIdInvalid = HardwareId(iota) - HardwareIdDigital1Input - HardwareIdAnalog1Input - HardwareIdDisplay1Output - HardwareIdButton1Input - HardwareIdButton2Input - HardwareIdKnob1Input - HardwareIdKnob2Input - HardwareIdVoltage1Output - HardwareIdVoltage2Output - HardwareIdVoltage3Output - HardwareIdVoltage4Output - HardwareIdVoltage5Output - HardwareIdVoltage6Output - // NOTE: always ONLY append to this list, NEVER remove, rename, or reorder -) - -// aliases for friendly internationali(s|z)ation -const ( - HardwareIdAnalogue1Input = HardwareIdAnalog1Input -) diff --git a/hardware/hal/displayoutput.go b/hardware/hal/displayoutput.go index 0f4a942..8d396af 100644 --- a/hardware/hal/displayoutput.go +++ b/hardware/hal/displayoutput.go @@ -8,3 +8,6 @@ type DisplayOutput interface { SetPixel(x, y int16, c color.RGBA) Display() error } + +type DisplayOutputConfig struct { +} diff --git a/hardware/hal/hal.go b/hardware/hal/hardware.go similarity index 53% rename from hardware/hal/hal.go rename to hardware/hal/hardware.go index 3ef761c..22f2bd5 100644 --- a/hardware/hal/hal.go +++ b/hardware/hal/hardware.go @@ -1,5 +1,6 @@ package hal +// HardwareId defines an identifier for specific hardware. See the README.md in the hardware directory for more details. type HardwareId int const ( @@ -22,7 +23,13 @@ const ( // NOTE: always ONLY append to this list, NEVER remove, rename, or reorder ) -// aliases for friendly internationali(s|z)ation +// aliases for friendly internationali(s|z)ation, colloquialisms, and naming conventions const ( HardwareIdAnalogue1Input = HardwareIdAnalog1Input + HardwareIdCV1Output = HardwareIdVoltage1Output + HardwareIdCV2Output = HardwareIdVoltage2Output + HardwareIdCV3Output = HardwareIdVoltage3Output + HardwareIdCV4Output = HardwareIdVoltage4Output + HardwareIdCV5Output = HardwareIdVoltage5Output + HardwareIdCV6Output = HardwareIdVoltage6Output ) 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 index 30d1e7a..92a0bec 100644 --- a/hardware/hal/revisionmarker.go +++ b/hardware/hal/revisionmarker.go @@ -1,14 +1,5 @@ package hal -type Revision int - -const ( - RevisionUnknown = Revision(iota) - Revision0 - Revision1 - Revision2 -) - type RevisionMarker interface { Revision() Revision } diff --git a/hardware/platform.go b/hardware/platform.go index 2c175f5..51a5f53 100644 --- a/hardware/platform.go +++ b/hardware/platform.go @@ -5,12 +5,14 @@ import ( "github.com/awonak/EuroPiGo/hardware/rev1" ) -func GetHardware[T any](revision Revision, id hal.HardwareId) T { +// 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 Revision1: + case hal.Revision1: return rev1.GetHardware[T](id) - case Revision2: + case hal.Revision2: // TODO: implement hardware design of rev2 return rev1.GetHardware[T](id) @@ -20,12 +22,14 @@ func GetHardware[T any](revision Revision, id hal.HardwareId) T { } } -func RevisionDetection() Revision { - for i := Revision0; i <= Revision2; i++ { +// RevisionDetection returns the best (most recent?) match for the hardware installed (or compiled for). +func RevisionDetection() hal.Revision { + // Iterate in reverse - try to find the newest revision that matches. + for i := hal.Revision2; i > hal.RevisionUnknown; i-- { if rd := GetHardware[hal.RevisionMarker](i, hal.HardwareIdRevisionMarker); rd != nil { // use the result of the call - don't just use `i` - in the event there's an alias or redirect involved return rd.Revision() } } - return RevisionUnknown + return hal.RevisionUnknown } diff --git a/hardware/rev1/README.md b/hardware/rev1/README.md new file mode 100644 index 0000000..c3f355b --- /dev/null +++ b/hardware/rev1/README.md @@ -0,0 +1,3 @@ +# hardware/rev1 + +This package is used for the [Original EuroPi hardware](https://github.com/Allen-Synthesis/EuroPi/tree/main/hardware). diff --git a/hardware/rev1/analoginput.go b/hardware/rev1/analoginput.go index 8b7bb41..ce87ae2 100644 --- a/hardware/rev1/analoginput.go +++ b/hardware/rev1/analoginput.go @@ -29,6 +29,13 @@ type analoginput struct { cal lerp.Lerper32[uint16] } +var ( + // static check + _ hal.AnalogInput = &analoginput{} + // silence linter + _ = newAnalogInput +) + type adcProvider interface { Get(samples int) uint16 } @@ -42,6 +49,7 @@ func newAnalogInput(adc adcProvider) *analoginput { } } +// 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") @@ -84,12 +92,12 @@ func (a *analoginput) ReadVOct() units.VOct { return units.VOct(a.ReadVoltage()) } -// MinVoltage returns the minimum voltage that that input can ever read +// MinVoltage returns the minimum voltage that that input can ever read by this device func (a *analoginput) MinVoltage() float32 { return MinInputVoltage } -// MaxVoltage returns the maximum voltage that the input can ever read +// MaxVoltage returns the maximum voltage that the input can ever read by this device func (a *analoginput) MaxVoltage() float32 { return MaxInputVoltage } diff --git a/hardware/rev1/digitalinput.go b/hardware/rev1/digitalinput.go index fc47073..2ed45b2 100644 --- a/hardware/rev1/digitalinput.go +++ b/hardware/rev1/digitalinput.go @@ -13,6 +13,13 @@ type digitalinput struct { lastChange time.Time } +var ( + // static check + _ hal.DigitalInput = &digitalinput{} + // silence linter + _ = newDigitalInput +) + type digitalReaderProvider interface { Get() bool SetHandler(changes hal.ChangeFlags, handler func()) @@ -26,6 +33,7 @@ func newDigitalInput(dr digitalReaderProvider) *digitalinput { } } +// Configure updates the device with various configuration parameters func (d *digitalinput) Configure(config hal.DigitalInputConfig) error { return nil } diff --git a/hardware/rev1/displayoutput.go b/hardware/rev1/displayoutput.go index 2f3b198..4127ee4 100644 --- a/hardware/rev1/displayoutput.go +++ b/hardware/rev1/displayoutput.go @@ -11,6 +11,13 @@ type displayoutput struct { dp displayProvider } +var ( + // static check + _ hal.DisplayOutput = &displayoutput{} + // silence linter + _ = newDisplayOutput +) + type displayProvider interface { ClearBuffer() Size() (x, y int16) @@ -25,17 +32,28 @@ func newDisplayOutput(dp displayProvider) hal.DisplayOutput { } } +// 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/rev1/messages.go b/hardware/rev1/messages.go index a8fe0c0..2f777f5 100644 --- a/hardware/rev1/messages.go +++ b/hardware/rev1/messages.go @@ -2,27 +2,33 @@ package rev1 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 } +// HwMessageDisplay represents a display update. type HwMessageDisplay struct { Op HwDisplayOp Operands []int16 } +// HwDisplayOp is the operation for a display update. type HwDisplayOp int const ( diff --git a/hardware/rev1/platform.go b/hardware/rev1/platform.go index 9800a3d..5d78ace 100644 --- a/hardware/rev1/platform.go +++ b/hardware/rev1/platform.go @@ -4,6 +4,8 @@ import ( "github.com/awonak/EuroPiGo/hardware/hal" ) +// These will be configured during `init()` from platform-specific files. +// See `pico.go` and `nonpico.go` for more information. var ( RevisionMarker hal.RevisionMarker InputDigital1 hal.DigitalInput @@ -22,6 +24,8 @@ var ( DeviceRandomGenerator1 hal.RandomGenerator ) +// 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 { switch hw { case hal.HardwareIdRevisionMarker: diff --git a/hardware/rev1/randomgenerator.go b/hardware/rev1/randomgenerator.go index 0ed6f06..4a00a45 100644 --- a/hardware/rev1/randomgenerator.go +++ b/hardware/rev1/randomgenerator.go @@ -8,6 +8,13 @@ type randomGenerator struct { rnd rndProvider } +var ( + // static check + _ hal.RandomGenerator = &randomGenerator{} + // silence linter + _ = newRandomGenerator +) + func newRandomGenerator(rnd rndProvider) hal.RandomGenerator { return &randomGenerator{ rnd: rnd, @@ -18,6 +25,7 @@ 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 { diff --git a/hardware/rev1/revisionmarker.go b/hardware/rev1/revisionmarker.go index 50abd06..98b0e3d 100644 --- a/hardware/rev1/revisionmarker.go +++ b/hardware/rev1/revisionmarker.go @@ -4,10 +4,18 @@ import "github.com/awonak/EuroPiGo/hardware/hal" type revisionMarker struct{} +var ( + // static check + _ hal.RevisionMarker = &revisionMarker{} + // silence linter + _ = newRevisionMarker +) + func newRevisionMarker() hal.RevisionMarker { return &revisionMarker{} } +// Revision returns the detected revision of the current hardware func (r *revisionMarker) Revision() hal.Revision { return hal.Revision1 } diff --git a/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go index 3bd028b..38002e8 100644 --- a/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -30,6 +30,19 @@ type voltageoutput struct { ofs uint16 } +var ( + // static check + _ hal.VoltageOutput = &voltageoutput{} + // silence linter + _ = newVoltageOuput +) + +type pwmProvider interface { + Configure(config hal.VoltageOutputConfig) error + Set(v float32, ofs uint16) + Get() float32 +} + // NewOutput returns a new Output interface. func newVoltageOuput(pwm pwmProvider) hal.VoltageOutput { o := &voltageoutput{ @@ -47,12 +60,7 @@ func newVoltageOuput(pwm pwmProvider) hal.VoltageOutput { return o } -type pwmProvider interface { - Configure(config hal.VoltageOutputConfig) error - Set(v float32, ofs uint16) - Get() float32 -} - +// 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 @@ -84,10 +92,12 @@ func (o *voltageoutput) Voltage() float32 { return o.pwm.Get() * MaxOutputVoltage } +// MinVoltage returns the minimum voltage this device will output func (o *voltageoutput) MinVoltage() float32 { return MinOutputVoltage } +// MaxVoltage returns the maximum voltage this device will output func (o *voltageoutput) MaxVoltage() float32 { return MaxOutputVoltage } diff --git a/hardware/revision.go b/hardware/revision.go deleted file mode 100644 index 8b1939c..0000000 --- a/hardware/revision.go +++ /dev/null @@ -1,19 +0,0 @@ -package hardware - -import "github.com/awonak/EuroPiGo/hardware/hal" - -type Revision = hal.Revision - -const ( - RevisionUnknown = hal.RevisionUnknown - Revision0 = hal.Revision0 - Revision1 = hal.Revision1 - Revision2 = hal.Revision2 -) - -// aliases -const ( - EuroPiProto = Revision0 - EuroPi = Revision1 - EuroPiX = Revision2 -) From 767bb7d9fd577c6b6143bd6c15f3b8bd59ec1103 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 18:48:57 -0700 Subject: [PATCH 24/62] Moved quantizer functions - added testing --- experimental/quantizer/quantizer.go | 6 - experimental/quantizer/quantizer_round.go | 25 --- experimental/quantizer/quantizer_trunc.go | 25 --- internal/testing/test.go | 188 ++++++++++++++++++ {experimental/quantizer => quantizer}/mode.go | 1 + quantizer/quantizer.go | 20 ++ quantizer/quantizer_round.go | 35 ++++ quantizer/quantizer_round_test.go | 111 +++++++++++ quantizer/quantizer_test.go | 17 ++ quantizer/quantizer_trunc.go | 36 ++++ quantizer/quantizer_trunc_test.go | 111 +++++++++++ 11 files changed, 519 insertions(+), 56 deletions(-) delete mode 100644 experimental/quantizer/quantizer.go delete mode 100644 experimental/quantizer/quantizer_round.go delete mode 100644 experimental/quantizer/quantizer_trunc.go create mode 100644 internal/testing/test.go rename {experimental/quantizer => quantizer}/mode.go (56%) create mode 100644 quantizer/quantizer.go create mode 100644 quantizer/quantizer_round.go create mode 100644 quantizer/quantizer_round_test.go create mode 100644 quantizer/quantizer_test.go create mode 100644 quantizer/quantizer_trunc.go create mode 100644 quantizer/quantizer_trunc_test.go diff --git a/experimental/quantizer/quantizer.go b/experimental/quantizer/quantizer.go deleted file mode 100644 index 77487d6..0000000 --- a/experimental/quantizer/quantizer.go +++ /dev/null @@ -1,6 +0,0 @@ -package quantizer - -type Quantizer[T any] interface { - QuantizeToIndex(in float32, length int) int - QuantizeToValue(in float32, list []T) T -} diff --git a/experimental/quantizer/quantizer_round.go b/experimental/quantizer/quantizer_round.go deleted file mode 100644 index f7a9395..0000000 --- a/experimental/quantizer/quantizer_round.go +++ /dev/null @@ -1,25 +0,0 @@ -package quantizer - -import ( - "github.com/awonak/EuroPiGo/lerp" -) - -type Round[T any] struct{} - -func (Round[T]) QuantizeToIndex(in float32, length int) int { - if length == 0 { - return -1 - } - - return lerp.NewLerp32(0, length-1).ClampedLerpRound(in) -} - -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/experimental/quantizer/quantizer_trunc.go b/experimental/quantizer/quantizer_trunc.go deleted file mode 100644 index dd6d9c3..0000000 --- a/experimental/quantizer/quantizer_trunc.go +++ /dev/null @@ -1,25 +0,0 @@ -package quantizer - -import ( - "github.com/awonak/EuroPiGo/lerp" -) - -type Trunc[T any] struct{} - -func (Trunc[T]) QuantizeToIndex(in float32, length int) int { - if length == 0 { - return -1 - } - - return lerp.NewLerp32(0, length-1).ClampedLerp(in) -} - -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/internal/testing/test.go b/internal/testing/test.go new file mode 100644 index 0000000..cacc6b0 --- /dev/null +++ b/internal/testing/test.go @@ -0,0 +1,188 @@ +//go:build !pico && test +// +build !pico,test + +package main + +import ( + "fmt" + "log" + "math" + "math/rand" + "time" + + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/experimental/screenbank" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" + "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" + "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" +) + +var ( + clock module.ClockGenerator + ui *screenbank.ScreenBank + screenMain = screen.Main{ + Clock: &clock, + } + screenSettings = screen.Settings{ + Clock: &clock, + } +) + +func startLoop(e *europi.EuroPi) { + if err := clock.Init(module.Config{ + BPM: 120.0, + GateDuration: time.Millisecond * 100, + Enabled: true, + ClockOut: func(value bool) { + if value { + e.CV1.SetCV(1.0) + } else { + e.CV1.SetCV(0.0) + } + europi.ForceRepaintUI(e) + }, + }); err != nil { + panic(err) + } +} + +func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { + clock.Tick(deltaTime) +} + +func panicHandler(e *europi.EuroPi, reason any) { + log.Fatalln(reason) +} + +func main() { + var err error + ui, err = screenbank.NewScreenBank( + screenbank.WithScreen("main", "\u2b50", &screenMain), + screenbank.WithScreen("settings", "\u2611", &screenSettings), + ) + if err != nil { + panic(err) + } + + // set up bus subscriptions + bus := rev1.DefaultEventBus + go func() { + diValueTicker := time.NewTicker(time.Second * 5) + defer diValueTicker.Stop() + diState := false + + aiValueTicker := time.NewTicker(time.Second * 4) + defer aiValueTicker.Stop() + + b1Ticker := time.NewTicker(time.Second * 1) + defer b1Ticker.Stop() + b1State := false + + b2Ticker := time.NewTicker(time.Second * 3) + defer b2Ticker.Stop() + b2State := false + + for { + select { + case <-diValueTicker.C: + value := rand.Float32() < 0.5 + bus.Post(fmt.Sprintf("hw_value_%d", hal.HardwareIdDigital1Input), rev1.HwMessageDigitalValue{ + Value: value, + }) + + if diState != value { + diState = value + if value { + // rising + bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdDigital1Input), rev1.HwMessageInterrupt{ + Change: hal.ChangeRising, + }) + } else { + // falling + bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdDigital1Input), rev1.HwMessageInterrupt{ + Change: hal.ChangeFalling, + }) + } + } + + case <-aiValueTicker.C: + bus.Post(fmt.Sprintf("hw_value_%d", hal.HardwareIdAnalog1Input), rev1.HwMessageADCValue{ + Value: uint16(rand.Int31n(math.MaxUint16)), + }) + + case <-b1Ticker.C: + value := rand.Float32() < 0.5 + bus.Post(fmt.Sprintf("hw_value_%d", hal.HardwareIdButton1Input), rev1.HwMessageDigitalValue{ + Value: value, + }) + + if b1State != value { + b1State = value + if value { + // rising + bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdButton1Input), rev1.HwMessageInterrupt{ + Change: hal.ChangeRising, + }) + } else { + // falling + bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdButton1Input), rev1.HwMessageInterrupt{ + Change: hal.ChangeFalling, + }) + } + } + + case <-b2Ticker.C: + value := rand.Float32() < 0.5 + bus.Post(fmt.Sprintf("hw_value_%d", hal.HardwareIdButton2Input), rev1.HwMessageDigitalValue{ + Value: value, + }) + + if b2State != value { + b2State = value + if value { + // rising + bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdButton2Input), rev1.HwMessageInterrupt{ + Change: hal.ChangeRising, + }) + } else { + // falling + bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdButton2Input), rev1.HwMessageInterrupt{ + Change: hal.ChangeFalling, + }) + } + } + } + } + }() + + for id := hal.HardwareIdVoltage1Output; id <= hal.HardwareIdVoltage6Output; id++ { + fn := func(hid hal.HardwareId) func(rev1.HwMessagePwmValue) { + return func(msg rev1.HwMessagePwmValue) { + log.Printf("CV%d: %v", hid-hal.HardwareIdVoltage1Output+1, msg.Value) + } + }(id) + event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", id), fn) + } + + event.Subscribe(bus, fmt.Sprintf("hw_display_%d", hal.HardwareIdDisplay1Output), func(msg rev1.HwMessageDisplay) { + if msg.Op == 1 { + return + } + log.Printf("display: %v(%+v)", msg.Op, msg.Operands) + }) + + // some options shown below are being explicitly set to their defaults + // only to showcase their existence. + europi.Bootstrap( + europi.EnableDisplayLogger(false), + //europi.BeginDestroy(panicHandler), + europi.InitRandom(true), + europi.StartLoop(startLoop), + europi.MainLoop(mainLoop), + europi.MainLoopInterval(time.Millisecond*1), + europi.UI(ui), + europi.UIRefreshRate(time.Millisecond*50), + ) +} diff --git a/experimental/quantizer/mode.go b/quantizer/mode.go similarity index 56% rename from experimental/quantizer/mode.go rename to quantizer/mode.go index d441d00..a66f0a7 100644 --- a/experimental/quantizer/mode.go +++ b/quantizer/mode.go @@ -1,5 +1,6 @@ package quantizer +// Mode specifies the kind of Quantizer function to be used. type Mode int const ( 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) + } + }) + }) + }) +} From be4978d781ab501fd6fe54bf037221c012a5191b Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 19:24:34 -0700 Subject: [PATCH 25/62] Better docs for hardware mappings. --- hardware/README.md | 2 +- hardware/rev1/README.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/hardware/README.md b/hardware/README.md index 4814f94..8574aaa 100644 --- a/hardware/README.md +++ b/hardware/README.md @@ -18,7 +18,7 @@ This package is used for obtaining singleton objects for particular hardware, id 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. -| Identifier | Alias | Interface | Notes | +| HardwareId | HardwareId Alias | Interface | 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. | diff --git a/hardware/rev1/README.md b/hardware/rev1/README.md index c3f355b..73582d6 100644 --- a/hardware/rev1/README.md +++ b/hardware/rev1/README.md @@ -1,3 +1,23 @@ # 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 | +|----|----|----|----| +| `RevisionMarker` | `hal.RevisionMarker` | `HardwareIdRevisionMarker` | | +| `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` | | From 10138b723c2c2f87c69c985e23ffeb4d18a18497 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 19:38:08 -0700 Subject: [PATCH 26/62] clean up linting - add some testing - add revision detection --- bootstrap.go | 2 +- bootstrap_features.go | 2 +- bootstrap_panic.go | 5 + bootstrap_uimodule.go | 2 +- europi.go | 10 +- europi_test.go | 23 ++++ experimental/fontwriter/writer.go | 4 +- internal/testing/test.go | 188 ------------------------------ 8 files changed, 42 insertions(+), 194 deletions(-) create mode 100644 europi_test.go delete mode 100644 internal/testing/test.go diff --git a/bootstrap.go b/bootstrap.go index ce566a3..13af2a9 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -180,7 +180,7 @@ func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, reason any) { if e != nil && e.Display != nil { // show the last buffer - e.Display.Display() + _ = e.Display.Display() } close(piWantDestroyChan) diff --git a/bootstrap_features.go b/bootstrap_features.go index 31eb766..b879c40 100644 --- a/bootstrap_features.go +++ b/bootstrap_features.go @@ -37,7 +37,7 @@ func flushDisplayLogger(e *EuroPi) { func initRandom(e *EuroPi) { if e.RND != nil { - e.RND.Configure(hal.RandomGeneratorConfig{}) + _ = e.RND.Configure(hal.RandomGeneratorConfig{}) } } diff --git a/bootstrap_panic.go b/bootstrap_panic.go index 5b06cf1..80a67e7 100644 --- a/bootstrap_panic.go +++ b/bootstrap_panic.go @@ -13,6 +13,11 @@ import ( // Not setting the build flag will set it to `handlePanicDisplayCrash` var DefaultPanicHandler func(e *EuroPi, reason any) +var ( + // silence linter + _ = handlePanicOnScreenLog +) + func handlePanicOnScreenLog(e *EuroPi, reason any) { if e == nil { // can't do anything if it's not enabled diff --git a/bootstrap_uimodule.go b/bootstrap_uimodule.go index 2cedd09..3cc0b67 100644 --- a/bootstrap_uimodule.go +++ b/bootstrap_uimodule.go @@ -41,7 +41,7 @@ func (u *uiModule) run(e *EuroPi, interval time.Duration) { u.logoPainter.PaintLogo(e, deltaTime) } u.screen.Paint(e, deltaTime) - disp.Display() + _ = disp.Display() } for { diff --git a/europi.go b/europi.go index 6f3c414..03effc9 100644 --- a/europi.go +++ b/europi.go @@ -26,9 +26,17 @@ type EuroPi struct { // New will return a new EuroPi struct. func New(opts ...hal.Revision) *EuroPi { - revision := hal.EuroPi + var revision hal.Revision if len(opts) > 0 { revision = opts[0] + } else { + // attempt to detect hardware revision + revision = hardware.RevisionDetection() + } + + if revision == hal.RevisionUnknown { + // could not detect revision + return nil } cv1 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage1Output) diff --git a/europi_test.go b/europi_test.go new file mode 100644 index 0000000..752f5fa --- /dev/null +++ b/europi_test.go @@ -0,0 +1,23 @@ +package europi_test + +import ( + "testing" + + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/hardware/hal" +) + +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 + if actual := europi.New(); actual != nil { + t.Fatal("EuroPi New: expected[nil] actual[non-nil]") + } + }) + + t.Run("Revision1", func(t *testing.T) { + if actual := europi.New(hal.Revision1); actual == nil { + t.Fatal("EuroPi New: expected[non-nil] actual[nil]") + } + }) +} diff --git a/experimental/fontwriter/writer.go b/experimental/fontwriter/writer.go index 7db82e0..77eaf0d 100644 --- a/experimental/fontwriter/writer.go +++ b/experimental/fontwriter/writer.go @@ -56,7 +56,7 @@ func (w *Writer) WriteLineInverse(text string, x, y int16, c color.RGBA) { 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) + _ = tinydraw.FilledRectangle(w.Display, x0, y0, int16(outerWidth+2), outerHeight, c) tinyfont.WriteLine(w.Display, w.Font, x1, y1, text, inverseC) } @@ -107,6 +107,6 @@ func (w *Writer) writeLineInverseAligned(text string, font tinyfont.Fonter, x, y B: ^c.B, A: c.A, } - tinydraw.FilledRectangle(w.Display, x0, y0, int16(outerWidth+2), outerHeight, c) + _ = tinydraw.FilledRectangle(w.Display, x0, y0, int16(outerWidth+2), outerHeight, c) tinyfont.WriteLine(w.Display, w.Font, x1, y1, text, inverseC) } diff --git a/internal/testing/test.go b/internal/testing/test.go deleted file mode 100644 index cacc6b0..0000000 --- a/internal/testing/test.go +++ /dev/null @@ -1,188 +0,0 @@ -//go:build !pico && test -// +build !pico,test - -package main - -import ( - "fmt" - "log" - "math" - "math/rand" - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/event" - "github.com/awonak/EuroPiGo/experimental/screenbank" - "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" - "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" - "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" -) - -var ( - clock module.ClockGenerator - ui *screenbank.ScreenBank - screenMain = screen.Main{ - Clock: &clock, - } - screenSettings = screen.Settings{ - Clock: &clock, - } -) - -func startLoop(e *europi.EuroPi) { - if err := clock.Init(module.Config{ - BPM: 120.0, - GateDuration: time.Millisecond * 100, - Enabled: true, - ClockOut: func(value bool) { - if value { - e.CV1.SetCV(1.0) - } else { - e.CV1.SetCV(0.0) - } - europi.ForceRepaintUI(e) - }, - }); err != nil { - panic(err) - } -} - -func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { - clock.Tick(deltaTime) -} - -func panicHandler(e *europi.EuroPi, reason any) { - log.Fatalln(reason) -} - -func main() { - var err error - ui, err = screenbank.NewScreenBank( - screenbank.WithScreen("main", "\u2b50", &screenMain), - screenbank.WithScreen("settings", "\u2611", &screenSettings), - ) - if err != nil { - panic(err) - } - - // set up bus subscriptions - bus := rev1.DefaultEventBus - go func() { - diValueTicker := time.NewTicker(time.Second * 5) - defer diValueTicker.Stop() - diState := false - - aiValueTicker := time.NewTicker(time.Second * 4) - defer aiValueTicker.Stop() - - b1Ticker := time.NewTicker(time.Second * 1) - defer b1Ticker.Stop() - b1State := false - - b2Ticker := time.NewTicker(time.Second * 3) - defer b2Ticker.Stop() - b2State := false - - for { - select { - case <-diValueTicker.C: - value := rand.Float32() < 0.5 - bus.Post(fmt.Sprintf("hw_value_%d", hal.HardwareIdDigital1Input), rev1.HwMessageDigitalValue{ - Value: value, - }) - - if diState != value { - diState = value - if value { - // rising - bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdDigital1Input), rev1.HwMessageInterrupt{ - Change: hal.ChangeRising, - }) - } else { - // falling - bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdDigital1Input), rev1.HwMessageInterrupt{ - Change: hal.ChangeFalling, - }) - } - } - - case <-aiValueTicker.C: - bus.Post(fmt.Sprintf("hw_value_%d", hal.HardwareIdAnalog1Input), rev1.HwMessageADCValue{ - Value: uint16(rand.Int31n(math.MaxUint16)), - }) - - case <-b1Ticker.C: - value := rand.Float32() < 0.5 - bus.Post(fmt.Sprintf("hw_value_%d", hal.HardwareIdButton1Input), rev1.HwMessageDigitalValue{ - Value: value, - }) - - if b1State != value { - b1State = value - if value { - // rising - bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdButton1Input), rev1.HwMessageInterrupt{ - Change: hal.ChangeRising, - }) - } else { - // falling - bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdButton1Input), rev1.HwMessageInterrupt{ - Change: hal.ChangeFalling, - }) - } - } - - case <-b2Ticker.C: - value := rand.Float32() < 0.5 - bus.Post(fmt.Sprintf("hw_value_%d", hal.HardwareIdButton2Input), rev1.HwMessageDigitalValue{ - Value: value, - }) - - if b2State != value { - b2State = value - if value { - // rising - bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdButton2Input), rev1.HwMessageInterrupt{ - Change: hal.ChangeRising, - }) - } else { - // falling - bus.Post(fmt.Sprintf("hw_interrupt_%d", hal.HardwareIdButton2Input), rev1.HwMessageInterrupt{ - Change: hal.ChangeFalling, - }) - } - } - } - } - }() - - for id := hal.HardwareIdVoltage1Output; id <= hal.HardwareIdVoltage6Output; id++ { - fn := func(hid hal.HardwareId) func(rev1.HwMessagePwmValue) { - return func(msg rev1.HwMessagePwmValue) { - log.Printf("CV%d: %v", hid-hal.HardwareIdVoltage1Output+1, msg.Value) - } - }(id) - event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", id), fn) - } - - event.Subscribe(bus, fmt.Sprintf("hw_display_%d", hal.HardwareIdDisplay1Output), func(msg rev1.HwMessageDisplay) { - if msg.Op == 1 { - return - } - log.Printf("display: %v(%+v)", msg.Op, msg.Operands) - }) - - // some options shown below are being explicitly set to their defaults - // only to showcase their existence. - europi.Bootstrap( - europi.EnableDisplayLogger(false), - //europi.BeginDestroy(panicHandler), - europi.InitRandom(true), - europi.StartLoop(startLoop), - europi.MainLoop(mainLoop), - europi.MainLoopInterval(time.Millisecond*1), - europi.UI(ui), - europi.UIRefreshRate(time.Millisecond*50), - ) -} From ba4c1b7e46300d4e514b693ca1d724ce0cd02823 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 23 Apr 2023 19:47:59 -0700 Subject: [PATCH 27/62] Better error handling on bad hardware detect --- bootstrap.go | 4 ++++ bootstrap_lifecycle.go | 8 ++++++++ bootstrap_uimodule.go | 4 ++++ experimental/knobbank/knobbank.go | 4 ++++ 4 files changed, 20 insertions(+) diff --git a/bootstrap.go b/bootstrap.go index 13af2a9..30c1876 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -44,6 +44,10 @@ func Bootstrap(options ...BootstrapOption) error { } e := config.europi + if e == nil { + return errors.New("no europi available") + } + Pi = e piWantDestroyChan = make(chan struct{}, 1) diff --git a/bootstrap_lifecycle.go b/bootstrap_lifecycle.go index 662e426..fa4273f 100644 --- a/bootstrap_lifecycle.go +++ b/bootstrap_lifecycle.go @@ -3,6 +3,10 @@ package europi import "time" func DefaultPostBootstrapInitialization(e *EuroPi) { + if e.Display == nil { + return + } + e.Display.ClearBuffer() if err := e.Display.Display(); err != nil { panic(err) @@ -10,6 +14,10 @@ func DefaultPostBootstrapInitialization(e *EuroPi) { } func DefaultBootstrapCompleted(e *EuroPi) { + if e.Display == nil { + return + } + e.Display.ClearBuffer() if err := e.Display.Display(); err != nil { panic(err) diff --git a/bootstrap_uimodule.go b/bootstrap_uimodule.go index 3cc0b67..8d4b2e7 100644 --- a/bootstrap_uimodule.go +++ b/bootstrap_uimodule.go @@ -59,6 +59,10 @@ func (u *uiModule) run(e *EuroPi, interval time.Duration) { } func (u *uiModule) setupButton(e *EuroPi, btn hal.ButtonInput, onShort func(e *EuroPi, value bool, deltaTime time.Duration), onLong func(e *EuroPi, deltaTime time.Duration)) { + if btn == nil { + return + } + if onShort == nil && onLong == nil { return } diff --git a/experimental/knobbank/knobbank.go b/experimental/knobbank/knobbank.go index 9b97c1c..709f309 100644 --- a/experimental/knobbank/knobbank.go +++ b/experimental/knobbank/knobbank.go @@ -16,6 +16,10 @@ type KnobBank struct { } func NewKnobBank(knob hal.KnobInput, opts ...KnobBankOption) (*KnobBank, error) { + if knob == nil { + return nil, errors.New("knob is nil") + } + kb := &KnobBank{ knob: knob, lastValue: knob.ReadVoltage(), From f477e4a7166b69dd447e42a5463df707dbecc7bb Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 24 Apr 2023 08:50:11 -0700 Subject: [PATCH 28/62] clean up panic handling - fix issues with ui lifecycle --- .gitignore | 1 + bootstrap.go | 26 +++++---- bootstrap_panic.go | 16 +++++- bootstrap_panicdisabled.go | 12 +++- bootstrap_ui.go | 78 ++----------------------- bootstrap_uimodule.go | 113 +++++++++++++++++++++++++++++++++---- 6 files changed, 149 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index 8196e5b..a3a2076 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.dll *.so *.dylib +__debug_bin # Test binary, built with `go test -c` *.test diff --git a/bootstrap.go b/bootstrap.go index 30c1876..fc0ac3b 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -10,7 +10,7 @@ var ( // Pi is a global EuroPi instance constructed by calling the Bootstrap() function Pi *EuroPi - piWantDestroyChan chan struct{} + piWantDestroyChan chan any ) // Bootstrap will set up a global runtime environment (see europi.Pi) @@ -49,7 +49,7 @@ func Bootstrap(options ...BootstrapOption) error { } Pi = e - piWantDestroyChan = make(chan struct{}, 1) + piWantDestroyChan = make(chan any, 1) var onceBootstrapDestroy sync.Once panicHandler := config.panicHandler @@ -85,12 +85,12 @@ func Bootstrap(options ...BootstrapOption) error { return nil } -func Shutdown() error { +func Shutdown(reason any) error { if piWantDestroyChan == nil { return errors.New("cannot shutdown: no available bootstrap") } - piWantDestroyChan <- struct{}{} + piWantDestroyChan <- reason return nil } @@ -138,15 +138,18 @@ func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { } func bootstrapRunLoopWithDelay(config *bootstrapConfig, e *EuroPi) { + if config.onMainLoopFn == nil { + panic(errors.New("no main loop specified")) + } + ticker := time.NewTicker(config.mainLoopInterval) defer ticker.Stop() lastTick := time.Now() -mainLoop: for { select { - case <-piWantDestroyChan: - break mainLoop + case reason := <-piWantDestroyChan: + panic(reason) case now := <-ticker.C: config.onMainLoopFn(e, now.Sub(lastTick)) @@ -156,12 +159,15 @@ mainLoop: } func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { + if config.onMainLoopFn == nil { + panic(errors.New("no main loop specified")) + } + lastTick := time.Now() -mainLoop: for { select { - case <-piWantDestroyChan: - break mainLoop + case reason := <-piWantDestroyChan: + panic(reason) default: now := time.Now() diff --git a/bootstrap_panic.go b/bootstrap_panic.go index 80a67e7..0a29022 100644 --- a/bootstrap_panic.go +++ b/bootstrap_panic.go @@ -3,6 +3,7 @@ package europi import ( "fmt" "log" + "os" "github.com/awonak/EuroPiGo/experimental/draw" "tinygo.org/x/tinydraw" @@ -28,9 +29,15 @@ func handlePanicOnScreenLog(e *EuroPi, reason any) { enableDisplayLogger(e) // show the panic on the screen - log.Panicln(fmt.Sprint(reason)) + log.Println(fmt.Sprint(reason)) flushDisplayLogger(e) + + os.Exit(1) +} + +func handlePanicLogger(e *EuroPi, reason any) { + log.Panic(reason) } func handlePanicDisplayCrash(e *EuroPi, reason any) { @@ -39,8 +46,13 @@ func handlePanicDisplayCrash(e *EuroPi, reason any) { return } - // display a diagonal line pattern through the screen to show that the EuroPi is crashed disp := e.Display + if disp == nil { + // can't do anything if we don't have a display + return + } + + // display a diagonal line pattern through the screen to show that the EuroPi is crashed width, height := disp.Size() ymax := height - 1 for x := -ymax; x < width; x += 4 { diff --git a/bootstrap_panicdisabled.go b/bootstrap_panicdisabled.go index 601c281..ed34c61 100644 --- a/bootstrap_panicdisabled.go +++ b/bootstrap_panicdisabled.go @@ -3,6 +3,16 @@ package europi +import ( + "github.com/awonak/EuroPiGo/hardware" + "github.com/awonak/EuroPiGo/hardware/hal" +) + func init() { - DefaultPanicHandler = handlePanicDisplayCrash + switch hardware.RevisionDetection() { + case hal.RevisionUnknown: + DefaultPanicHandler = handlePanicLogger + default: + DefaultPanicHandler = handlePanicDisplayCrash + } } diff --git a/bootstrap_ui.go b/bootstrap_ui.go index f48452c..fec4dd3 100644 --- a/bootstrap_ui.go +++ b/bootstrap_ui.go @@ -2,8 +2,6 @@ package europi import ( "time" - - "github.com/awonak/EuroPiGo/debounce" ) type UserInterface interface { @@ -52,67 +50,9 @@ var ( ) func enableUI(e *EuroPi, screen UserInterface, interval time.Duration) { - ui.screen = screen - if ui.screen == nil { - return - } - - ui.logoPainter, _ = screen.(UserInterfaceLogoPainter) - - ui.repaint = make(chan struct{}, 1) - - var ( - inputB1 func(e *EuroPi, value bool, deltaTime time.Duration) - inputB1L func(e *EuroPi, deltaTime time.Duration) - ) - if in, ok := screen.(UserInterfaceButton1); ok { - var debounceDelay time.Duration - if db, ok := screen.(UserInterfaceButton1Debounce); ok { - debounceDelay = db.Button1Debounce() - } - inputDB := debounce.NewDebouncer(func(value bool, deltaTime time.Duration) { - if !value { - in.Button1(e, deltaTime) - } - }).Debounce(debounceDelay) - inputB1 = func(e *EuroPi, value bool, deltaTime time.Duration) { - inputDB(value) - } - } else if in, ok := screen.(UserInterfaceButton1Ex); ok { - inputB1 = in.Button1Ex - } - if in, ok := screen.(UserInterfaceButton1Long); ok { - inputB1L = in.Button1Long - } - ui.setupButton(e, e.B1, inputB1, inputB1L) - - var ( - inputB2 func(e *EuroPi, value bool, deltaTime time.Duration) - inputB2L func(e *EuroPi, deltaTime time.Duration) - ) - if in, ok := screen.(UserInterfaceButton2); ok { - var debounceDelay time.Duration - if db, ok := screen.(UserInterfaceButton2Debounce); ok { - debounceDelay = db.Button2Debounce() - } - inputDB := debounce.NewDebouncer(func(value bool, deltaTime time.Duration) { - if !value { - in.Button2(e, deltaTime) - } - }).Debounce(debounceDelay) - inputB2 = func(e *EuroPi, value bool, deltaTime time.Duration) { - inputDB(value) - } - } else if in, ok := screen.(UserInterfaceButton2Ex); ok { - inputB2 = in.Button2Ex - } - if in, ok := screen.(UserInterfaceButton2Long); ok { - inputB2L = in.Button2Long - } - ui.setupButton(e, e.B2, inputB2, inputB2L) + ui.setup(e, screen) - ui.wg.Add(1) - go ui.run(e, interval) + ui.start(e, interval) } func startUI(e *EuroPi) { @@ -125,19 +65,9 @@ func startUI(e *EuroPi) { // ForceRepaintUI schedules a forced repaint of the UI (if it is configured and running) func ForceRepaintUI(e *EuroPi) { - if ui.repaint != nil { - ui.repaint <- struct{}{} - } + ui.repaint() } func disableUI(e *EuroPi) { - if ui.stop != nil { - ui.stop() - } - - if ui.repaint != nil { - close(ui.repaint) - } - - ui.wait() + ui.shutdown() } diff --git a/bootstrap_uimodule.go b/bootstrap_uimodule.go index 8d4b2e7..9c9b571 100644 --- a/bootstrap_uimodule.go +++ b/bootstrap_uimodule.go @@ -5,24 +5,116 @@ import ( "sync" "time" + "github.com/awonak/EuroPiGo/debounce" "github.com/awonak/EuroPiGo/hardware/hal" ) type uiModule struct { screen UserInterface logoPainter UserInterfaceLogoPainter - repaint chan struct{} + repaintCh chan struct{} stop context.CancelFunc wg sync.WaitGroup } +func (u *uiModule) setup(e *EuroPi, screen UserInterface) { + ui.screen = screen + if ui.screen == nil { + return + } + + ui.logoPainter, _ = screen.(UserInterfaceLogoPainter) + + ui.repaintCh = make(chan struct{}, 1) + + var ( + inputB1 func(e *EuroPi, value bool, deltaTime time.Duration) + inputB1L func(e *EuroPi, deltaTime time.Duration) + ) + if in, ok := screen.(UserInterfaceButton1); ok { + var debounceDelay time.Duration + if db, ok := screen.(UserInterfaceButton1Debounce); ok { + debounceDelay = db.Button1Debounce() + } + inputDB := debounce.NewDebouncer(func(value bool, deltaTime time.Duration) { + if !value { + in.Button1(e, deltaTime) + } + }).Debounce(debounceDelay) + inputB1 = func(e *EuroPi, value bool, deltaTime time.Duration) { + inputDB(value) + } + } else if in, ok := screen.(UserInterfaceButton1Ex); ok { + inputB1 = in.Button1Ex + } + if in, ok := screen.(UserInterfaceButton1Long); ok { + inputB1L = in.Button1Long + } + ui.setupButton(e, e.B1, inputB1, inputB1L) + + var ( + inputB2 func(e *EuroPi, value bool, deltaTime time.Duration) + inputB2L func(e *EuroPi, deltaTime time.Duration) + ) + if in, ok := screen.(UserInterfaceButton2); ok { + var debounceDelay time.Duration + if db, ok := screen.(UserInterfaceButton2Debounce); ok { + debounceDelay = db.Button2Debounce() + } + inputDB := debounce.NewDebouncer(func(value bool, deltaTime time.Duration) { + if !value { + in.Button2(e, deltaTime) + } + }).Debounce(debounceDelay) + inputB2 = func(e *EuroPi, value bool, deltaTime time.Duration) { + inputDB(value) + } + } else if in, ok := screen.(UserInterfaceButton2Ex); ok { + inputB2 = in.Button2Ex + } + if in, ok := screen.(UserInterfaceButton2Long); ok { + inputB2L = in.Button2Long + } + ui.setupButton(e, e.B2, inputB2, inputB2L) +} + +func (u *uiModule) start(e *EuroPi, interval time.Duration) { + ui.wg.Add(1) + go ui.run(e, interval) +} + func (u *uiModule) wait() { u.wg.Wait() } +func (u *uiModule) repaint() { + if u.repaintCh != nil { + u.repaintCh <- struct{}{} + } +} + +func (u *uiModule) shutdown() { + if u.stop != nil { + u.stop() + } + + if ui.repaintCh != nil { + close(ui.repaintCh) + } + + ui.wait() +} + func (u *uiModule) run(e *EuroPi, interval time.Duration) { defer u.wg.Done() + disp := e.Display + if disp == nil { + // no display means no ui + // TODO: make uiModule work when any user input/output is specified, not just display + return + } + ctx, cancel := context.WithCancel(context.Background()) ui.stop = cancel defer ui.stop() @@ -30,12 +122,7 @@ func (u *uiModule) run(e *EuroPi, interval time.Duration) { t := time.NewTicker(interval) defer t.Stop() - disp := e.Display - lastTime := time.Now() - - paint := func(now time.Time) { - deltaTime := now.Sub(lastTime) - lastTime = now + paint := func(deltaTime time.Duration) { disp.ClearBuffer() if u.logoPainter != nil { u.logoPainter.PaintLogo(e, deltaTime) @@ -44,16 +131,22 @@ func (u *uiModule) run(e *EuroPi, interval time.Duration) { _ = disp.Display() } + lastTime := time.Now() for { select { case <-ctx.Done(): return - case <-ui.repaint: - paint(time.Now()) + case <-ui.repaintCh: + now := time.Now() + deltaTime := now.Sub(lastTime) + lastTime = now + paint(deltaTime) case now := <-t.C: - paint(now) + deltaTime := now.Sub(lastTime) + lastTime = now + paint(deltaTime) } } } From 4be9641a25ce9926ca6294bfdf82ae2abb4407a5 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 24 Apr 2023 09:20:27 -0700 Subject: [PATCH 29/62] simple testing rig for running on non-pico hw --- hardware/rev1/nonpico.go | 4 +- internal/nonpico/events/listeners.go | 86 ++++++++++++++++++++++++++++ internal/nonpico/main.go | 78 +++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 internal/nonpico/events/listeners.go create mode 100644 internal/nonpico/main.go diff --git a/hardware/rev1/nonpico.go b/hardware/rev1/nonpico.go index e4cc87b..8a7d1e3 100644 --- a/hardware/rev1/nonpico.go +++ b/hardware/rev1/nonpico.go @@ -1,5 +1,5 @@ -//go:build !pico && test -// +build !pico,test +//go:build !pico && revision1 +// +build !pico,revision1 package rev1 diff --git a/internal/nonpico/events/listeners.go b/internal/nonpico/events/listeners.go new file mode 100644 index 0000000..3c9963a --- /dev/null +++ b/internal/nonpico/events/listeners.go @@ -0,0 +1,86 @@ +//go:build !pico && revision1 +// +build !pico,revision1 + +package events + +import ( + "fmt" + "log" + "math" + "sync" + + "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" + "github.com/awonak/EuroPiGo/lerp" +) + +var ( + voLerp = lerp.NewLerp32[uint16](0, math.MaxUint16) +) + +func SetupVoltageOutputListeners() { + bus := rev1.DefaultEventBus + + for id := hal.HardwareIdVoltage1Output; id <= hal.HardwareIdVoltage6Output; id++ { + fn := func(hid hal.HardwareId) func(rev1.HwMessagePwmValue) { + return func(msg rev1.HwMessagePwmValue) { + v := voLerp.ClampedInverseLerp(msg.Value) * rev1.MaxOutputVoltage + log.Printf("CV%d: %v", hid-hal.HardwareIdVoltage1Output+1, v) + } + }(id) + event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", id), fn) + } +} + +func SetupDisplayOutputListener() { + bus := rev1.DefaultEventBus + event.Subscribe(bus, fmt.Sprintf("hw_display_%d", hal.HardwareIdDisplay1Output), func(msg rev1.HwMessageDisplay) { + if msg.Op == 1 { + return + } + log.Printf("display: %v(%+v)", msg.Op, msg.Operands) + }) + +} + +var ( + states sync.Map +) + +func SetDigitalInput(id hal.HardwareId, value bool) { + prevState, _ := states.Load(id) + + bus := rev1.DefaultEventBus + + states.Store(id, value) + bus.Post(fmt.Sprintf("hw_value_%d", id), rev1.HwMessageDigitalValue{ + Value: value, + }) + + if prevState != value { + if value { + // rising + bus.Post(fmt.Sprintf("hw_interrupt_%d", id), rev1.HwMessageInterrupt{ + Change: hal.ChangeRising, + }) + } else { + // falling + bus.Post(fmt.Sprintf("hw_interrupt_%d", id), rev1.HwMessageInterrupt{ + Change: hal.ChangeFalling, + }) + } + } +} + +var ( + aiLerp = lerp.NewLerp32[uint16](rev1.DefaultCalibratedMinAI, rev1.DefaultCalibratedMaxAI) +) + +func SetAnalogInput(id hal.HardwareId, voltage float32) { + bus := rev1.DefaultEventBus + + bus.Post(fmt.Sprintf("hw_value_%d", id), rev1.HwMessageADCValue{ + Value: aiLerp.Lerp(voltage), + }) +} diff --git a/internal/nonpico/main.go b/internal/nonpico/main.go new file mode 100644 index 0000000..da0246e --- /dev/null +++ b/internal/nonpico/main.go @@ -0,0 +1,78 @@ +//go:build !pico && revision1 +// +build !pico,revision1 + +package main + +import ( + "log" + "time" + + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/experimental/screenbank" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/internal/nonpico/events" + "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" + "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" +) + +var ( + clock module.ClockGenerator + ui *screenbank.ScreenBank + screenMain = screen.Main{ + Clock: &clock, + } + screenSettings = screen.Settings{ + Clock: &clock, + } +) + +func startLoop(e *europi.EuroPi) { + clock.Init(module.Config{ + BPM: 120.0, + GateDuration: time.Millisecond * 100, + Enabled: true, + ClockOut: func(value bool) { + if value { + e.CV1.SetCV(1.0) + } else { + e.CV1.SetCV(0.0) + } + europi.ForceRepaintUI(e) + }, + }) + + events.SetupVoltageOutputListeners() +} + +func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { + clock.Tick(deltaTime) +} + +func main() { + var err error + ui, err = screenbank.NewScreenBank( + screenbank.WithScreen("main", "\u2b50", &screenMain), + screenbank.WithScreen("settings", "\u2611", &screenSettings), + ) + if err != nil { + panic(err) + } + + // some options shown below are being explicitly set to their defaults + // only to showcase their existence. + err = europi.Bootstrap( + europi.UsingEuroPi(europi.New(hal.Revision1)), + europi.EnableDisplayLogger(false), + europi.InitRandom(true), + europi.StartLoop(startLoop), + europi.MainLoop(mainLoop), + europi.MainLoopInterval(time.Millisecond*1), + europi.UI(ui), + europi.UIRefreshRate(time.Millisecond*50), + ) + if err != nil { + log.Fatalf("Bootstrap exited with: %v\n", err) + } + + log.Println("done.") +} From f07436444c48df7845ba39538b031d5c7edf409f Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 24 Apr 2023 17:30:51 -0700 Subject: [PATCH 30/62] added simulator of europi - see europi.AttachNonPicoWS() for more details --- bootstrap.go | 25 +++- bootstrap_features.go | 7 + bootstrap_nonpico.go | 146 +++++++++++++++++++ bootstrapoptions.go | 11 +- bootstrapoptions_features.go | 10 ++ go.mod | 2 + go.sum | 2 + internal/nonpico/events/listeners.go | 15 +- internal/nonpico/main.go | 78 ---------- internal/nonpico/site/index.html | 205 +++++++++++++++++++++++++++ internal/nonpico/ws/websocket.go | 101 +++++++++++++ 11 files changed, 505 insertions(+), 97 deletions(-) create mode 100644 bootstrap_nonpico.go delete mode 100644 internal/nonpico/main.go create mode 100644 internal/nonpico/site/index.html create mode 100644 internal/nonpico/ws/websocket.go diff --git a/bootstrap.go b/bootstrap.go index fc0ac3b..5abdd2b 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -1,6 +1,7 @@ package europi import ( + "context" "errors" "sync" "time" @@ -51,7 +52,10 @@ func Bootstrap(options ...BootstrapOption) error { Pi = e piWantDestroyChan = make(chan any, 1) - var onceBootstrapDestroy sync.Once + var ( + onceBootstrapDestroy sync.Once + cancel context.CancelFunc + ) panicHandler := config.panicHandler lastDestroyFunc := config.onBeginDestroyFn runBootstrapDestroy := func() { @@ -65,7 +69,7 @@ func Bootstrap(options ...BootstrapOption) error { } } onceBootstrapDestroy.Do(func() { - bootstrapDestroy(&config, e, reason) + bootstrapDestroy(&config, e, cancel, reason) }) } defer runBootstrapDestroy() @@ -74,7 +78,7 @@ func Bootstrap(options ...BootstrapOption) error { config.onPostBootstrapConstructionFn(e) } - bootstrapInitializeComponents(&config, e) + cancel = bootstrapInitializeComponents(&config, e) if config.onBootstrapCompletedFn != nil { config.onBootstrapCompletedFn(e) @@ -94,7 +98,7 @@ func Shutdown(reason any) error { return nil } -func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) { +func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) context.CancelFunc { if config.onPreInitializeComponentsFn != nil { config.onPreInitializeComponentsFn(e) } @@ -103,6 +107,11 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) { enableDisplayLogger(e) } + var cancel context.CancelFunc + if config.enableNonPicoWebSocket && activateNonPicoWebSocket != nil { + activateNonPicoWebSocket(e) + } + if config.initRandom { initRandom(e) } @@ -115,6 +124,8 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) { if config.onPostInitializeComponentsFn != nil { config.onPostInitializeComponentsFn(e) } + + return cancel } func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { @@ -177,13 +188,17 @@ func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { } } -func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, reason any) { +func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, cancel context.CancelFunc, reason any) { if config.onBeginDestroyFn != nil { config.onBeginDestroyFn(e, reason) } disableUI(e) + if config.enableNonPicoWebSocket && deactivateNonPicoWebSocket != nil { + deactivateNonPicoWebSocket(e, cancel) + } + disableDisplayLogger(e) uninitRandom(e) diff --git a/bootstrap_features.go b/bootstrap_features.go index b879c40..5f6a79f 100644 --- a/bootstrap_features.go +++ b/bootstrap_features.go @@ -1,6 +1,7 @@ package europi import ( + "context" "log" "os" @@ -43,3 +44,9 @@ func initRandom(e *EuroPi) { func uninitRandom(e *EuroPi) { } + +// used for non-pico testing of bootstrapped europi apps +var ( + activateNonPicoWebSocket func(e *EuroPi) (context.Context, context.CancelFunc) + deactivateNonPicoWebSocket func(e *EuroPi, cancel context.CancelFunc) +) diff --git a/bootstrap_nonpico.go b/bootstrap_nonpico.go new file mode 100644 index 0000000..dddd90b --- /dev/null +++ b/bootstrap_nonpico.go @@ -0,0 +1,146 @@ +//go:build !pico && revision1 +// +build !pico,revision1 + +package europi + +import ( + "context" + "embed" + "encoding/json" + "io/fs" + "log" + "net/http" + + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" + "github.com/awonak/EuroPiGo/internal/nonpico/events" + "github.com/awonak/EuroPiGo/internal/nonpico/ws" +) + +//go:embed internal/nonpico/site +var nonpicoSiteContent embed.FS + +func nonPicoActivateWebSocket(e *EuroPi) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + defer cancel() + + subFS, _ := fs.Sub(nonpicoSiteContent, "internal/nonpico/site") + http.Handle("/", http.FileServer(http.FS(subFS))) + http.HandleFunc("/ws", nonPicoApiHandler) + if err := http.ListenAndServe(":8080", nil); err != nil { + panic(err) + } + }() + + return ctx, cancel +} + +func nonPicoDeactivateWebSocket(e *EuroPi, cancel context.CancelFunc) { + cancel() +} + +func nonPicoApiHandler(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL, "nonPicoApiHandler") + + 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() + + type voltageOutput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Voltage float32 `json:"voltage"` + } + events.SetupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) { + _ = sock.WriteJSON(voltageOutput{ + Kind: "voltageOutput", + HardwareId: id, + Voltage: voltage, + }) + }) + + type displayOutput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Op rev1.HwDisplayOp `json:"op"` + Params []int16 `json:"params"` + } + + events.SetupDisplayOutputListener(func(id hal.HardwareId, op rev1.HwDisplayOp, params []int16) { + _ = sock.WriteJSON(displayOutput{ + Kind: "displayOutput", + HardwareId: id, + Op: op, + Params: params, + }) + }) + + type kind struct { + Kind string `json:"kind"` + } + + type setDigitalInput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Value bool `json:"value"` + } + + type setAnalogInput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Voltage float32 `json:"voltage"` + } + + 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 setDigitalInput + if err := json.Unmarshal(blob, &di); err != nil { + sock.SetError(err) + break + } + events.SetDigitalInput(di.HardwareId, di.Value) + + case "setAnalogInput": + var ai setAnalogInput + if err := json.Unmarshal(blob, &ai); err != nil { + sock.SetError(err) + break + } + events.SetAnalogInput(ai.HardwareId, ai.Voltage) + + default: + // ignore + } + } +} + +func init() { + activateNonPicoWebSocket = nonPicoActivateWebSocket + deactivateNonPicoWebSocket = nonPicoDeactivateWebSocket +} diff --git a/bootstrapoptions.go b/bootstrapoptions.go index 2ed1174..292baba 100644 --- a/bootstrapoptions.go +++ b/bootstrapoptions.go @@ -8,11 +8,12 @@ import ( type BootstrapOption func(o *bootstrapConfig) error type bootstrapConfig struct { - mainLoopInterval time.Duration - panicHandler func(e *EuroPi, reason any) - enableDisplayLogger bool - initRandom bool - europi *EuroPi + mainLoopInterval time.Duration + panicHandler func(e *EuroPi, reason any) + enableDisplayLogger bool + initRandom bool + europi *EuroPi + enableNonPicoWebSocket bool // user interface ui UserInterface diff --git a/bootstrapoptions_features.go b/bootstrapoptions_features.go index a76084b..debd7e0 100644 --- a/bootstrapoptions_features.go +++ b/bootstrapoptions_features.go @@ -50,6 +50,7 @@ func InitRandom(enabled bool) BootstrapOption { } } +// UsingEuroPi sets a specific EuroPi object instance for all operations in the bootstrap func UsingEuroPi(e *EuroPi) BootstrapOption { return func(o *bootstrapConfig) error { if e == nil { @@ -60,3 +61,12 @@ func UsingEuroPi(e *EuroPi) BootstrapOption { return nil } } + +// AttachNonPicoWS (if enabled and on non-Pico builds with build flags of `-tags=revision1` set) +// starts up a websocket interface and system debugger on port 8080 +func AttachNonPicoWS(enabled bool) BootstrapOption { + return func(o *bootstrapConfig) error { + o.enableNonPicoWebSocket = enabled + return nil + } +} diff --git a/go.mod b/go.mod index 86e8466..c126ef8 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,5 @@ require ( tinygo.org/x/tinydraw v0.3.0 tinygo.org/x/tinyfont v0.3.0 ) + +require github.com/gorilla/websocket v1.5.0 // indirect diff --git a/go.sum b/go.sum index e599d43..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= diff --git a/internal/nonpico/events/listeners.go b/internal/nonpico/events/listeners.go index 3c9963a..7277253 100644 --- a/internal/nonpico/events/listeners.go +++ b/internal/nonpico/events/listeners.go @@ -5,7 +5,6 @@ package events import ( "fmt" - "log" "math" "sync" @@ -19,27 +18,25 @@ var ( voLerp = lerp.NewLerp32[uint16](0, math.MaxUint16) ) -func SetupVoltageOutputListeners() { +func SetupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) { bus := rev1.DefaultEventBus for id := hal.HardwareIdVoltage1Output; id <= hal.HardwareIdVoltage6Output; id++ { fn := func(hid hal.HardwareId) func(rev1.HwMessagePwmValue) { return func(msg rev1.HwMessagePwmValue) { v := voLerp.ClampedInverseLerp(msg.Value) * rev1.MaxOutputVoltage - log.Printf("CV%d: %v", hid-hal.HardwareIdVoltage1Output+1, v) + cb(hid, v) } }(id) event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", id), fn) } } -func SetupDisplayOutputListener() { +func SetupDisplayOutputListener(cb func(id hal.HardwareId, op rev1.HwDisplayOp, params []int16)) { bus := rev1.DefaultEventBus - event.Subscribe(bus, fmt.Sprintf("hw_display_%d", hal.HardwareIdDisplay1Output), func(msg rev1.HwMessageDisplay) { - if msg.Op == 1 { - return - } - log.Printf("display: %v(%+v)", msg.Op, msg.Operands) + id := hal.HardwareIdDisplay1Output + event.Subscribe(bus, fmt.Sprintf("hw_display_%d", id), func(msg rev1.HwMessageDisplay) { + cb(id, msg.Op, msg.Operands) }) } diff --git a/internal/nonpico/main.go b/internal/nonpico/main.go deleted file mode 100644 index da0246e..0000000 --- a/internal/nonpico/main.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build !pico && revision1 -// +build !pico,revision1 - -package main - -import ( - "log" - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/experimental/screenbank" - "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/internal/nonpico/events" - "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" - "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" -) - -var ( - clock module.ClockGenerator - ui *screenbank.ScreenBank - screenMain = screen.Main{ - Clock: &clock, - } - screenSettings = screen.Settings{ - Clock: &clock, - } -) - -func startLoop(e *europi.EuroPi) { - clock.Init(module.Config{ - BPM: 120.0, - GateDuration: time.Millisecond * 100, - Enabled: true, - ClockOut: func(value bool) { - if value { - e.CV1.SetCV(1.0) - } else { - e.CV1.SetCV(0.0) - } - europi.ForceRepaintUI(e) - }, - }) - - events.SetupVoltageOutputListeners() -} - -func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { - clock.Tick(deltaTime) -} - -func main() { - var err error - ui, err = screenbank.NewScreenBank( - screenbank.WithScreen("main", "\u2b50", &screenMain), - screenbank.WithScreen("settings", "\u2611", &screenSettings), - ) - if err != nil { - panic(err) - } - - // some options shown below are being explicitly set to their defaults - // only to showcase their existence. - err = europi.Bootstrap( - europi.UsingEuroPi(europi.New(hal.Revision1)), - europi.EnableDisplayLogger(false), - europi.InitRandom(true), - europi.StartLoop(startLoop), - europi.MainLoop(mainLoop), - europi.MainLoopInterval(time.Millisecond*1), - europi.UI(ui), - europi.UIRefreshRate(time.Millisecond*50), - ) - if err != nil { - log.Fatalf("Bootstrap exited with: %v\n", err) - } - - log.Println("done.") -} diff --git a/internal/nonpico/site/index.html b/internal/nonpico/site/index.html new file mode 100644 index 0000000..287bf0d --- /dev/null +++ b/internal/nonpico/site/index.html @@ -0,0 +1,205 @@ + + + + + + EuroPi Tester + + + + + +
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + \ No newline at end of file diff --git a/internal/nonpico/ws/websocket.go b/internal/nonpico/ws/websocket.go new file mode 100644 index 0000000..4cc8439 --- /dev/null +++ b/internal/nonpico/ws/websocket.go @@ -0,0 +1,101 @@ +//go:build !pico && revision1 +// +build !pico,revision1 + +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(context.Background()) + + conn.SetReadLimit(2048) + _ = conn.SetReadDeadline(time.Now().Add(pongWait)) + conn.SetPongHandler(func(appData string) error { + return conn.SetReadDeadline(time.Now().Add(pongWait)) + }) + + ws := &WebSocket{ + conn: conn, + ctx: ctx, + cancel: cancel, + } + + return ws, nil +} From a5c330fc26b6b87827f56f552a957aca2b08fdb5 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 24 Apr 2023 17:52:46 -0700 Subject: [PATCH 31/62] Fix for read timeout issue - was intended to be a write timeout --- internal/nonpico/ws/websocket.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/nonpico/ws/websocket.go b/internal/nonpico/ws/websocket.go index 4cc8439..bd8fccf 100644 --- a/internal/nonpico/ws/websocket.go +++ b/internal/nonpico/ws/websocket.go @@ -86,9 +86,9 @@ func Upgrade(w http.ResponseWriter, r *http.Request) (*WebSocket, error) { ctx, cancel := context.WithCancel(context.Background()) conn.SetReadLimit(2048) - _ = conn.SetReadDeadline(time.Now().Add(pongWait)) + _ = conn.SetWriteDeadline(time.Now().Add(pongWait)) conn.SetPongHandler(func(appData string) error { - return conn.SetReadDeadline(time.Now().Add(pongWait)) + return conn.SetWriteDeadline(time.Now().Add(pongWait)) }) ws := &WebSocket{ From 62e18f0e29a7d71819a10054ae24cb6fe69d0b45 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 24 Apr 2023 20:35:27 -0700 Subject: [PATCH 32/62] clean up websocket code - api is now specific to revision of europi --- bootstrap.go | 19 +-- bootstrap_features.go | 9 +- bootstrap_nonpico.go | 142 ++-------------- europi.go | 4 + internal/nonpico/rev1/api.go | 154 ++++++++++++++++++ .../nonpico/{ => rev1}/events/listeners.go | 0 internal/nonpico/{ => rev1}/site/index.html | 0 internal/nonpico/wsactivator.go | 23 +++ 8 files changed, 206 insertions(+), 145 deletions(-) create mode 100644 internal/nonpico/rev1/api.go rename internal/nonpico/{ => rev1}/events/listeners.go (100%) rename internal/nonpico/{ => rev1}/site/index.html (100%) create mode 100644 internal/nonpico/wsactivator.go diff --git a/bootstrap.go b/bootstrap.go index 5abdd2b..fc71449 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -1,7 +1,6 @@ package europi import ( - "context" "errors" "sync" "time" @@ -54,7 +53,7 @@ func Bootstrap(options ...BootstrapOption) error { var ( onceBootstrapDestroy sync.Once - cancel context.CancelFunc + nonPicoWSApi nonPicoWSActivation ) panicHandler := config.panicHandler lastDestroyFunc := config.onBeginDestroyFn @@ -69,7 +68,7 @@ func Bootstrap(options ...BootstrapOption) error { } } onceBootstrapDestroy.Do(func() { - bootstrapDestroy(&config, e, cancel, reason) + bootstrapDestroy(&config, e, nonPicoWSApi, reason) }) } defer runBootstrapDestroy() @@ -78,7 +77,7 @@ func Bootstrap(options ...BootstrapOption) error { config.onPostBootstrapConstructionFn(e) } - cancel = bootstrapInitializeComponents(&config, e) + nonPicoWSApi = bootstrapInitializeComponents(&config, e) if config.onBootstrapCompletedFn != nil { config.onBootstrapCompletedFn(e) @@ -98,7 +97,7 @@ func Shutdown(reason any) error { return nil } -func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) context.CancelFunc { +func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) nonPicoWSActivation { if config.onPreInitializeComponentsFn != nil { config.onPreInitializeComponentsFn(e) } @@ -107,9 +106,9 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) context.C enableDisplayLogger(e) } - var cancel context.CancelFunc + var nonPicoWSApi nonPicoWSActivation if config.enableNonPicoWebSocket && activateNonPicoWebSocket != nil { - activateNonPicoWebSocket(e) + nonPicoWSApi = activateNonPicoWebSocket(e) } if config.initRandom { @@ -125,7 +124,7 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) context.C config.onPostInitializeComponentsFn(e) } - return cancel + return nonPicoWSApi } func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { @@ -188,7 +187,7 @@ func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { } } -func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, cancel context.CancelFunc, reason any) { +func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, nonPicoWSApi nonPicoWSActivation, reason any) { if config.onBeginDestroyFn != nil { config.onBeginDestroyFn(e, reason) } @@ -196,7 +195,7 @@ func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, cancel context.CancelF disableUI(e) if config.enableNonPicoWebSocket && deactivateNonPicoWebSocket != nil { - deactivateNonPicoWebSocket(e, cancel) + deactivateNonPicoWebSocket(e, nonPicoWSApi) } disableDisplayLogger(e) diff --git a/bootstrap_features.go b/bootstrap_features.go index 5f6a79f..0abb6a8 100644 --- a/bootstrap_features.go +++ b/bootstrap_features.go @@ -1,7 +1,6 @@ package europi import ( - "context" "log" "os" @@ -47,6 +46,10 @@ func uninitRandom(e *EuroPi) { // used for non-pico testing of bootstrapped europi apps var ( - activateNonPicoWebSocket func(e *EuroPi) (context.Context, context.CancelFunc) - deactivateNonPicoWebSocket func(e *EuroPi, cancel context.CancelFunc) + activateNonPicoWebSocket func(e *EuroPi) nonPicoWSActivation + deactivateNonPicoWebSocket func(e *EuroPi, api nonPicoWSActivation) ) + +type nonPicoWSActivation interface { + Shutdown() error +} diff --git a/bootstrap_nonpico.go b/bootstrap_nonpico.go index dddd90b..5828ef5 100644 --- a/bootstrap_nonpico.go +++ b/bootstrap_nonpico.go @@ -1,141 +1,19 @@ -//go:build !pico && revision1 -// +build !pico,revision1 +//go:build !pico +// +build !pico package europi -import ( - "context" - "embed" - "encoding/json" - "io/fs" - "log" - "net/http" +import "github.com/awonak/EuroPiGo/internal/nonpico" - "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" - "github.com/awonak/EuroPiGo/internal/nonpico/events" - "github.com/awonak/EuroPiGo/internal/nonpico/ws" -) - -//go:embed internal/nonpico/site -var nonpicoSiteContent embed.FS - -func nonPicoActivateWebSocket(e *EuroPi) (context.Context, context.CancelFunc) { - ctx, cancel := context.WithCancel(context.Background()) - - go func() { - defer cancel() - - subFS, _ := fs.Sub(nonpicoSiteContent, "internal/nonpico/site") - http.Handle("/", http.FileServer(http.FS(subFS))) - http.HandleFunc("/ws", nonPicoApiHandler) - if err := http.ListenAndServe(":8080", nil); err != nil { - panic(err) - } - }() - - return ctx, cancel -} - -func nonPicoDeactivateWebSocket(e *EuroPi, cancel context.CancelFunc) { - cancel() +func nonPicoActivateWebSocket(e *EuroPi) nonPicoWSActivation { + nonPicoWSApi := nonpico.ActivateWebSocket(e.Revision) + return nonPicoWSApi } -func nonPicoApiHandler(w http.ResponseWriter, r *http.Request) { - log.Println(r.URL, "nonPicoApiHandler") - - 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() - - type voltageOutput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Voltage float32 `json:"voltage"` - } - events.SetupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) { - _ = sock.WriteJSON(voltageOutput{ - Kind: "voltageOutput", - HardwareId: id, - Voltage: voltage, - }) - }) - - type displayOutput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Op rev1.HwDisplayOp `json:"op"` - Params []int16 `json:"params"` - } - - events.SetupDisplayOutputListener(func(id hal.HardwareId, op rev1.HwDisplayOp, params []int16) { - _ = sock.WriteJSON(displayOutput{ - Kind: "displayOutput", - HardwareId: id, - Op: op, - Params: params, - }) - }) - - type kind struct { - Kind string `json:"kind"` - } - - type setDigitalInput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Value bool `json:"value"` - } - - type setAnalogInput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Voltage float32 `json:"voltage"` - } - - 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 setDigitalInput - if err := json.Unmarshal(blob, &di); err != nil { - sock.SetError(err) - break - } - events.SetDigitalInput(di.HardwareId, di.Value) - - case "setAnalogInput": - var ai setAnalogInput - if err := json.Unmarshal(blob, &ai); err != nil { - sock.SetError(err) - break - } - events.SetAnalogInput(ai.HardwareId, ai.Voltage) - - default: - // ignore +func nonPicoDeactivateWebSocket(e *EuroPi, nonPicoWSApi nonPicoWSActivation) { + if nonPicoWSApi != nil { + if err := nonPicoWSApi.Shutdown(); err != nil { + panic(err) } } } diff --git a/europi.go b/europi.go index 03effc9..ea46042 100644 --- a/europi.go +++ b/europi.go @@ -7,6 +7,8 @@ import ( // EuroPi is the collection of component wrappers used to interact with the module. type EuroPi struct { + Revision hal.Revision + Display hal.DisplayOutput DI hal.DigitalInput AI hal.AnalogInput @@ -47,6 +49,8 @@ func New(opts ...hal.Revision) *EuroPi { cv6 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage6Output) e := &EuroPi{ + Revision: revision, + Display: hardware.GetHardware[hal.DisplayOutput](revision, hal.HardwareIdDisplay1Output), DI: hardware.GetHardware[hal.DigitalInput](revision, hal.HardwareIdDigital1Input), diff --git a/internal/nonpico/rev1/api.go b/internal/nonpico/rev1/api.go new file mode 100644 index 0000000..8753bea --- /dev/null +++ b/internal/nonpico/rev1/api.go @@ -0,0 +1,154 @@ +//bob //go:build !pico +//bob // +build !pico + +package rev1 + +import ( + "context" + "embed" + "encoding/json" + "io/fs" + "log" + "net/http" + + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" + "github.com/awonak/EuroPiGo/internal/nonpico/rev1/events" + "github.com/awonak/EuroPiGo/internal/nonpico/ws" +) + +//go:embed site +var nonPicoSiteContent embed.FS + +type WSActivation struct { + ctx context.Context + cancel context.CancelFunc +} + +func (a *WSActivation) Shutdown() error { + if a.cancel != nil { + a.cancel() + } + return nil +} + +func ActivateWebSocket() *WSActivation { + ctx, cancel := context.WithCancel(context.Background()) + + a := &WSActivation{ + ctx: ctx, + cancel: cancel, + } + + go func() { + defer cancel() + + subFS, _ := fs.Sub(nonPicoSiteContent, "site") + http.Handle("/", http.FileServer(http.FS(subFS))) + http.HandleFunc("/ws", apiHandler) + if err := http.ListenAndServe(":8080", nil); err != nil { + panic(err) + } + }() + + return a +} + +func apiHandler(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL, "rev1.apiHandler") + + 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() + + type voltageOutput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Voltage float32 `json:"voltage"` + } + events.SetupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) { + _ = sock.WriteJSON(voltageOutput{ + Kind: "voltageOutput", + HardwareId: id, + Voltage: voltage, + }) + }) + + type displayOutput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Op rev1.HwDisplayOp `json:"op"` + Params []int16 `json:"params"` + } + + events.SetupDisplayOutputListener(func(id hal.HardwareId, op rev1.HwDisplayOp, params []int16) { + _ = sock.WriteJSON(displayOutput{ + Kind: "displayOutput", + HardwareId: id, + Op: op, + Params: params, + }) + }) + + type kind struct { + Kind string `json:"kind"` + } + + type setDigitalInput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Value bool `json:"value"` + } + + type setAnalogInput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Voltage float32 `json:"voltage"` + } + + 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 setDigitalInput + if err := json.Unmarshal(blob, &di); err != nil { + sock.SetError(err) + break + } + events.SetDigitalInput(di.HardwareId, di.Value) + + case "setAnalogInput": + var ai setAnalogInput + if err := json.Unmarshal(blob, &ai); err != nil { + sock.SetError(err) + break + } + events.SetAnalogInput(ai.HardwareId, ai.Voltage) + + default: + // ignore + } + } +} diff --git a/internal/nonpico/events/listeners.go b/internal/nonpico/rev1/events/listeners.go similarity index 100% rename from internal/nonpico/events/listeners.go rename to internal/nonpico/rev1/events/listeners.go diff --git a/internal/nonpico/site/index.html b/internal/nonpico/rev1/site/index.html similarity index 100% rename from internal/nonpico/site/index.html rename to internal/nonpico/rev1/site/index.html diff --git a/internal/nonpico/wsactivator.go b/internal/nonpico/wsactivator.go new file mode 100644 index 0000000..66966eb --- /dev/null +++ b/internal/nonpico/wsactivator.go @@ -0,0 +1,23 @@ +//go:build !pico +// +build !pico + +package nonpico + +import ( + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/internal/nonpico/rev1" +) + +type WSActivation interface { + Shutdown() error +} + +func ActivateWebSocket(revision hal.Revision) WSActivation { + switch revision { + case hal.Revision1: + return rev1.ActivateWebSocket() + + default: + return nil + } +} From 40ad1ec74bac059553e86c95003628552265fed7 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 24 Apr 2023 20:54:35 -0700 Subject: [PATCH 33/62] adding pprof - fix build tag --- internal/nonpico/rev1/api.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/nonpico/rev1/api.go b/internal/nonpico/rev1/api.go index 8753bea..d03fb95 100644 --- a/internal/nonpico/rev1/api.go +++ b/internal/nonpico/rev1/api.go @@ -1,5 +1,5 @@ -//bob //go:build !pico -//bob // +build !pico +//go:build !pico +// +build !pico package rev1 @@ -10,6 +10,7 @@ import ( "io/fs" "log" "net/http" + _ "net/http/pprof" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev1" From 846357a46e4b752d7c3c269953b1d694f6acd545 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 25 Apr 2023 09:06:16 -0700 Subject: [PATCH 34/62] clean up websocket api - improve performance of display output by buffering on server side - change analog inputs into sliders --- internal/nonpico/rev1/api.go | 162 ++++-------------------- internal/nonpico/rev1/displaymode.go | 11 ++ internal/nonpico/rev1/site/index.html | 95 +++++++++++--- internal/nonpico/rev1/wsactivation.go | 174 ++++++++++++++++++++++++++ 4 files changed, 289 insertions(+), 153 deletions(-) create mode 100644 internal/nonpico/rev1/displaymode.go create mode 100644 internal/nonpico/rev1/wsactivation.go diff --git a/internal/nonpico/rev1/api.go b/internal/nonpico/rev1/api.go index d03fb95..22b0ab4 100644 --- a/internal/nonpico/rev1/api.go +++ b/internal/nonpico/rev1/api.go @@ -4,152 +4,40 @@ package rev1 import ( - "context" - "embed" - "encoding/json" - "io/fs" - "log" - "net/http" - _ "net/http/pprof" - "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev1" - "github.com/awonak/EuroPiGo/internal/nonpico/rev1/events" - "github.com/awonak/EuroPiGo/internal/nonpico/ws" ) -//go:embed site -var nonPicoSiteContent embed.FS - -type WSActivation struct { - ctx context.Context - cancel context.CancelFunc +type voltageOutput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Voltage float32 `json:"voltage"` } -func (a *WSActivation) Shutdown() error { - if a.cancel != nil { - a.cancel() - } - return nil +// displayMode = displayModeSeparate (0) +type displayOutput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Op rev1.HwDisplayOp `json:"op"` + Params []int16 `json:"params"` } -func ActivateWebSocket() *WSActivation { - ctx, cancel := context.WithCancel(context.Background()) - - a := &WSActivation{ - ctx: ctx, - cancel: cancel, - } - - go func() { - defer cancel() - - subFS, _ := fs.Sub(nonPicoSiteContent, "site") - http.Handle("/", http.FileServer(http.FS(subFS))) - http.HandleFunc("/ws", apiHandler) - if err := http.ListenAndServe(":8080", nil); err != nil { - panic(err) - } - }() - - return a +// displayMode = displayModeCombined (1) +type displayScreenOuptut struct { + Kind string `json:"kind"` + Width int `json:"width"` + Height int `json:"height"` + Data []byte `json:"data"` } -func apiHandler(w http.ResponseWriter, r *http.Request) { - log.Println(r.URL, "rev1.apiHandler") - - 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() - - type voltageOutput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Voltage float32 `json:"voltage"` - } - events.SetupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) { - _ = sock.WriteJSON(voltageOutput{ - Kind: "voltageOutput", - HardwareId: id, - Voltage: voltage, - }) - }) - - type displayOutput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Op rev1.HwDisplayOp `json:"op"` - Params []int16 `json:"params"` - } - - events.SetupDisplayOutputListener(func(id hal.HardwareId, op rev1.HwDisplayOp, params []int16) { - _ = sock.WriteJSON(displayOutput{ - Kind: "displayOutput", - HardwareId: id, - Op: op, - Params: params, - }) - }) - - type kind struct { - Kind string `json:"kind"` - } - - type setDigitalInput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Value bool `json:"value"` - } - - type setAnalogInput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Voltage float32 `json:"voltage"` - } - - 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 setDigitalInput - if err := json.Unmarshal(blob, &di); err != nil { - sock.SetError(err) - break - } - events.SetDigitalInput(di.HardwareId, di.Value) - - case "setAnalogInput": - var ai setAnalogInput - if err := json.Unmarshal(blob, &ai); err != nil { - sock.SetError(err) - break - } - events.SetAnalogInput(ai.HardwareId, ai.Voltage) +type setDigitalInput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Value bool `json:"value"` +} - default: - // ignore - } - } +type setAnalogInput struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Voltage float32 `json:"voltage"` } diff --git a/internal/nonpico/rev1/displaymode.go b/internal/nonpico/rev1/displaymode.go new file mode 100644 index 0000000..bac3585 --- /dev/null +++ b/internal/nonpico/rev1/displaymode.go @@ -0,0 +1,11 @@ +//go:build !pico +// +build !pico + +package rev1 + +type displayMode int + +const ( + displayModeSeparate = displayMode(iota) + displayModeCombined +) diff --git a/internal/nonpico/rev1/site/index.html b/internal/nonpico/rev1/site/index.html index 287bf0d..35d420f 100644 --- a/internal/nonpico/rev1/site/index.html +++ b/internal/nonpico/rev1/site/index.html @@ -15,6 +15,7 @@ const hw2 = document.getElementById("hw2"); // Digital Input const hw3 = document.getElementById("hw3"); // Analog Input + const hw3Disp = document.getElementById("hw3-text"); // Analog Input value display const hw5 = document.getElementById("hw5"); // Button 1 const hw6 = document.getElementById("hw6"); // Button 2 const hw7 = document.getElementById("hw7"); // Knob 1 @@ -47,8 +48,15 @@ setDigitalInputValue(2, false); }); + hw3.addEventListener('change', function(evt) { + var v = evt.target.value/6553.5 + setAnalogInputValue(3, v) + hw3Disp.innerText = v.toPrecision(3) + " Volts" + }); hw3.addEventListener('input', function(evt) { - setAnalogInputValue(3, evt.target.value) + var v = evt.target.value/6553.5 + setAnalogInputValue(3, v) + hw3Disp.innerText = v.toPrecision(3) + " Volts" }); hw5.addEventListener('mousedown', function(evt) { @@ -65,15 +73,21 @@ setDigitalInputValue(6, false); }); + hw7.addEventListener('change', function(evt) { + setAnalogInputValue(7, evt.target.value/65535) + }); hw7.addEventListener('input', function(evt) { - setAnalogInputValue(7, evt.target.value) + setAnalogInputValue(7, evt.target.value/65535) }); + hw8.addEventListener('change', function(evt) { + setAnalogInputValue(8, evt.target.value/65535) + }); hw8.addEventListener('input', function(evt) { - setAnalogInputValue(8, evt.target.value) + setAnalogInputValue(8, evt.target.value/65535) }); - function displayOutput(msg) { + function displayOutput(msg) { // displayMode=0 switch (msg.op) { case 0: // HwDisplayOpClearBuffer displayBuffer = document.createElement("canvas"); @@ -94,6 +108,33 @@ } } + function displayScreenOutput(msg) { // displayMode=1 + var byteCharacters = atob(msg.data); + var i = 0; + for (var y = 0; y < msg.height; y++) { + if (y >= hw4.height) { + break; + } + var pos = y * hw4.width * 4; + for (var x = 0; x < msg.width; x++) { + if (x >= hw4.width) { + i += (msg.width - x) * 4; + break; + } + for (var c = 0; c < 4; c++) { + if (y < hw4.height && x < hw4.width) { + var v = (i < byteCharacters.length) + ? byteCharacters.charCodeAt(i) + : 0; + imageData.data[pos++] = v; + } + i++ + } + } + } + primaryCtx.putImageData(imageData, 0, 0); + } + function processMessage(blob) { var msg = JSON.parse(blob); if (typeof msg != "object") { @@ -105,6 +146,10 @@ displayOutput(msg); break; + case "displayScreenOutput": + displayScreenOutput(msg); + break; + case "voltageOutput": var item = document.getElementById(`hw${msg.hardwareId}`); if (item) { @@ -117,7 +162,10 @@ } if (window["WebSocket"]) { - conn = new WebSocket("ws://" + document.location.host + "/ws"); + // displayMode: + // 0 = [default] displayOutput message (each operation is separate) + // 1 = displayScreenOutput message (full screen content as one message) + conn = new WebSocket("ws://" + document.location.host + "/ws?displayMode=1"); conn.onclose = function (evt) { var item = document.getElementById("body"); item.innerHTML += "

Connection closed.

"; @@ -133,16 +181,30 @@ }; @@ -151,13 +213,14 @@ -
+
- + +

5.00 Volts

-
+
- +
@@ -168,13 +231,13 @@
-
+
- +
-
+
- +
diff --git a/internal/nonpico/rev1/wsactivation.go b/internal/nonpico/rev1/wsactivation.go new file mode 100644 index 0000000..2fb9b93 --- /dev/null +++ b/internal/nonpico/rev1/wsactivation.go @@ -0,0 +1,174 @@ +//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/hardware/rev1" + "github.com/awonak/EuroPiGo/internal/nonpico/rev1/events" + "github.com/awonak/EuroPiGo/internal/nonpico/ws" +) + +type WSActivation struct { + ctx context.Context + cancel context.CancelFunc + displayMode displayMode +} + +func ActivateWebSocket() *WSActivation { + a := &WSActivation{} + + a.Start(context.Background()) + + 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) + + 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 = 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() + + events.SetupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) { + _ = sock.WriteJSON(voltageOutput{ + Kind: "voltageOutput", + HardwareId: id, + Voltage: voltage, + }) + }) + + displayWidth, displayHeight := 128, 32 + displayScreenOutputMsg := displayScreenOuptut{ + Kind: "displayScreenOutput", + Width: displayWidth, + Height: displayHeight, + Data: make([]byte, displayWidth*displayHeight*4), + } + events.SetupDisplayOutputListener(func(id hal.HardwareId, op rev1.HwDisplayOp, params []int16) { + switch a.displayMode { + case displayModeCombined: + switch op { + case rev1.HwDisplayOpClearBuffer: + for i := range displayScreenOutputMsg.Data { + displayScreenOutputMsg.Data[i] = 0 + } + case rev1.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 rev1.HwDisplayOpDisplay: + _ = sock.WriteJSON(displayScreenOutputMsg) + default: + } + + default: + _ = sock.WriteJSON(displayOutput{ + 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 setDigitalInput + if err := json.Unmarshal(blob, &di); err != nil { + sock.SetError(err) + break + } + events.SetDigitalInput(di.HardwareId, di.Value) + + case "setAnalogInput": + var ai setAnalogInput + if err := json.Unmarshal(blob, &ai); err != nil { + sock.SetError(err) + break + } + events.SetAnalogInput(ai.HardwareId, ai.Voltage) + + default: + // ignore + } + } +} From f72e2698d310e4e4fbf551f41261676d51a0b1ec Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 25 Apr 2023 22:27:46 -0700 Subject: [PATCH 35/62] Rework code to support hardware detection - move pico and non-pico code into their own spaces - fix some race conditions with hardware initialization --- bootstrap_panicdisabled.go | 12 +- europi.go | 22 +- hardware/hal/revisionmarker.go | 26 ++ hardware/platform.go | 59 ++++- hardware/rev1/analoginput.go | 6 +- hardware/rev1/digitalinput.go | 6 +- hardware/rev1/displayoutput.go | 6 +- hardware/rev1/nonpico.go | 179 -------------- hardware/rev1/pico.go | 234 ------------------ hardware/rev1/platform.go | 46 +++- hardware/rev1/randomgenerator.go | 6 +- hardware/rev1/revisionmarker.go | 21 -- hardware/rev1/voltageoutput.go | 6 +- internal/nonpico/nonpico.go | 31 +++ internal/nonpico/rev1.go | 14 ++ internal/nonpico/rev1/adc.go | 37 +++ internal/nonpico/rev1/api.go | 19 +- internal/nonpico/rev1/digitalreader.go | 42 ++++ internal/nonpico/rev1/displayoutput.go | 59 +++++ .../nonpico/rev1/{events => }/listeners.go | 37 ++- .../nonpico}/rev1/messages.go | 0 internal/nonpico/rev1/platform.go | 25 ++ internal/nonpico/rev1/pwm.go | 45 ++++ internal/nonpico/rev1/wsactivation.go | 26 +- internal/nonpico/rev2.go | 14 ++ internal/nonpico/ws/websocket.go | 4 +- internal/pico/adc.go | 44 ++++ internal/pico/digitalreader.go | 53 ++++ internal/pico/display.go | 59 +++++ internal/pico/pico.go | 50 ++++ internal/pico/pwm.go | 76 ++++++ internal/pico/revisiondetection.go | 56 +++++ internal/pico/rnd.go | 21 ++ nonpico.go | 14 ++ pico.go | 14 ++ 35 files changed, 845 insertions(+), 524 deletions(-) delete mode 100644 hardware/rev1/nonpico.go delete mode 100644 hardware/rev1/pico.go delete mode 100644 hardware/rev1/revisionmarker.go create mode 100644 internal/nonpico/nonpico.go create mode 100644 internal/nonpico/rev1.go create mode 100644 internal/nonpico/rev1/adc.go create mode 100644 internal/nonpico/rev1/digitalreader.go create mode 100644 internal/nonpico/rev1/displayoutput.go rename internal/nonpico/rev1/{events => }/listeners.go (54%) rename {hardware => internal/nonpico}/rev1/messages.go (100%) create mode 100644 internal/nonpico/rev1/platform.go create mode 100644 internal/nonpico/rev1/pwm.go create mode 100644 internal/nonpico/rev2.go create mode 100644 internal/pico/adc.go create mode 100644 internal/pico/digitalreader.go create mode 100644 internal/pico/display.go create mode 100644 internal/pico/pico.go create mode 100644 internal/pico/pwm.go create mode 100644 internal/pico/revisiondetection.go create mode 100644 internal/pico/rnd.go create mode 100644 nonpico.go create mode 100644 pico.go diff --git a/bootstrap_panicdisabled.go b/bootstrap_panicdisabled.go index ed34c61..92b8eba 100644 --- a/bootstrap_panicdisabled.go +++ b/bootstrap_panicdisabled.go @@ -9,10 +9,12 @@ import ( ) func init() { - switch hardware.RevisionDetection() { - case hal.RevisionUnknown: - DefaultPanicHandler = handlePanicLogger - default: - DefaultPanicHandler = handlePanicDisplayCrash + hardware.OnRevisionDetected <- func(revision hal.Revision) { + switch revision { + case hal.RevisionUnknown: + DefaultPanicHandler = handlePanicLogger + default: + DefaultPanicHandler = handlePanicDisplayCrash + } } } diff --git a/europi.go b/europi.go index ea46042..3c2563c 100644 --- a/europi.go +++ b/europi.go @@ -26,21 +26,23 @@ type EuroPi struct { RND hal.RandomGenerator } -// New will return a new EuroPi struct. -func New(opts ...hal.Revision) *EuroPi { - var revision hal.Revision - if len(opts) > 0 { - revision = opts[0] - } else { - // attempt to detect hardware revision - revision = hardware.RevisionDetection() - } +// New will return a new EuroPi struct based on the detected hardware revision +func New() *EuroPi { + // 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) *EuroPi { if revision == hal.RevisionUnknown { - // could not detect revision + // unknown revision return nil } + // this will block until the hardware components are initialized + hardware.WaitForReady() + cv1 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage1Output) cv2 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage2Output) cv3 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage3Output) diff --git a/hardware/hal/revisionmarker.go b/hardware/hal/revisionmarker.go index 92a0bec..68049eb 100644 --- a/hardware/hal/revisionmarker.go +++ b/hardware/hal/revisionmarker.go @@ -3,3 +3,29 @@ 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/platform.go b/hardware/platform.go index 51a5f53..b6b17c9 100644 --- a/hardware/platform.go +++ b/hardware/platform.go @@ -1,6 +1,9 @@ package hardware import ( + "sync" + "sync/atomic" + "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev1" ) @@ -22,14 +25,54 @@ func GetHardware[T any](revision hal.Revision, id hal.HardwareId) T { } } -// RevisionDetection returns the best (most recent?) match for the hardware installed (or compiled for). -func RevisionDetection() hal.Revision { - // Iterate in reverse - try to find the newest revision that matches. - for i := hal.Revision2; i > hal.RevisionUnknown; i-- { - if rd := GetHardware[hal.RevisionMarker](i, hal.HardwareIdRevisionMarker); rd != nil { - // use the result of the call - don't just use `i` - in the event there's an alias or redirect involved - return rd.Revision() +var ( + onRevisionDetected = make(chan func(revision hal.Revision), 10) + OnRevisionDetected chan<- func(revision hal.Revision) = onRevisionDetected + revisionWgDone sync.Once + hardwareReady atomic.Value + hardwareReadyMu sync.Mutex + hardwareReadyCond = sync.NewCond(&hardwareReadyMu) +) + +func SetDetectedRevision(opts ...hal.Revision) { + // 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()) + } + } + }() + }) +} + +func SetReady() { + hardwareReady.Store(true) + hardwareReadyCond.Broadcast() +} + +func WaitForReady() { + hardwareReadyCond.L.Lock() + for { + ready := hardwareReady.Load() + if v, ok := ready.(bool); v && ok { + break } + hardwareReadyCond.Wait() + } + hardwareReadyCond.L.Unlock() +} + +func GetRevision() hal.Revision { + var waitForDetect sync.WaitGroup + waitForDetect.Add(1) + var detectedRevision hal.Revision + OnRevisionDetected <- func(revision hal.Revision) { + detectedRevision = revision + waitForDetect.Done() } - return hal.RevisionUnknown + waitForDetect.Wait() + return detectedRevision } diff --git a/hardware/rev1/analoginput.go b/hardware/rev1/analoginput.go index ce87ae2..0ac3347 100644 --- a/hardware/rev1/analoginput.go +++ b/hardware/rev1/analoginput.go @@ -24,7 +24,7 @@ const ( // 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 + adc ADCProvider samples int cal lerp.Lerper32[uint16] } @@ -36,12 +36,12 @@ var ( _ = newAnalogInput ) -type adcProvider interface { +type ADCProvider interface { Get(samples int) uint16 } // newAnalogInput creates a new Analog Input -func newAnalogInput(adc adcProvider) *analoginput { +func newAnalogInput(adc ADCProvider) *analoginput { return &analoginput{ adc: adc, samples: DefaultSamples, diff --git a/hardware/rev1/digitalinput.go b/hardware/rev1/digitalinput.go index 2ed45b2..506544c 100644 --- a/hardware/rev1/digitalinput.go +++ b/hardware/rev1/digitalinput.go @@ -9,7 +9,7 @@ import ( // digitalinput is a struct for handling reading of the digital input. type digitalinput struct { - dr digitalReaderProvider + dr DigitalReaderProvider lastChange time.Time } @@ -20,13 +20,13 @@ var ( _ = newDigitalInput ) -type digitalReaderProvider interface { +type DigitalReaderProvider interface { Get() bool SetHandler(changes hal.ChangeFlags, handler func()) } // newDigitalInput creates a new digital input struct. -func newDigitalInput(dr digitalReaderProvider) *digitalinput { +func newDigitalInput(dr DigitalReaderProvider) *digitalinput { return &digitalinput{ dr: dr, lastChange: time.Now(), diff --git a/hardware/rev1/displayoutput.go b/hardware/rev1/displayoutput.go index 4127ee4..3d84dee 100644 --- a/hardware/rev1/displayoutput.go +++ b/hardware/rev1/displayoutput.go @@ -8,7 +8,7 @@ import ( // displayoutput is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED. type displayoutput struct { - dp displayProvider + dp DisplayProvider } var ( @@ -18,7 +18,7 @@ var ( _ = newDisplayOutput ) -type displayProvider interface { +type DisplayProvider interface { ClearBuffer() Size() (x, y int16) SetPixel(x, y int16, c color.RGBA) @@ -26,7 +26,7 @@ type displayProvider interface { } // newDisplayOutput returns a new Display struct. -func newDisplayOutput(dp displayProvider) hal.DisplayOutput { +func newDisplayOutput(dp DisplayProvider) hal.DisplayOutput { return &displayoutput{ dp: dp, } diff --git a/hardware/rev1/nonpico.go b/hardware/rev1/nonpico.go deleted file mode 100644 index 8a7d1e3..0000000 --- a/hardware/rev1/nonpico.go +++ /dev/null @@ -1,179 +0,0 @@ -//go:build !pico && revision1 -// +build !pico,revision1 - -package rev1 - -import ( - "fmt" - "image/color" - "math" - - "github.com/awonak/EuroPiGo/event" - "github.com/awonak/EuroPiGo/hardware/hal" -) - -var ( - DefaultEventBus = event.NewBus() -) - -//============= ADC =============// - -type nonPicoAdc struct { - bus event.Bus - id hal.HardwareId - value uint16 -} - -func newNonPicoAdc(bus event.Bus, id hal.HardwareId) adcProvider { - adc := &nonPicoAdc{ - bus: bus, - 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) -} - -//============= DigitalReader =============// - -type nonPicoDigitalReader struct { - bus event.Bus - id hal.HardwareId - value bool - handler func() -} - -func newNonPicoDigitalReader(bus event.Bus, id hal.HardwareId) digitalReaderProvider { - dr := &nonPicoDigitalReader{ - bus: bus, - id: id, - } - 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(d.bus, fmt.Sprintf("hw_interrupt_%d", d.id), func(msg HwMessageInterrupt) { - if (msg.Change & changes) != 0 { - handler() - } - }) -} - -//============= PWM =============// - -type nonPicoPwm struct { - bus event.Bus - id hal.HardwareId - v float32 -} - -func newNonPicoPwm(bus event.Bus, id hal.HardwareId) pwmProvider { - p := &nonPicoPwm{ - bus: bus, - id: id, - } - return p -} - -func (p *nonPicoPwm) Configure(config hal.VoltageOutputConfig) error { - return nil -} - -func (p *nonPicoPwm) Set(v float32, ofs uint16) { - invertedV := v * math.MaxUint16 - // volts := (float32(o.pwm.Top()) - invertedCv) - o.ofs - volts := invertedV - float32(ofs) - p.v = v - p.bus.Post(fmt.Sprintf("hw_pwm_%d", p.id), HwMessagePwmValue{ - Value: uint16(volts), - }) -} - -func (p *nonPicoPwm) Get() float32 { - return p.v -} - -//============= Display =============// - -const ( - oledWidth = 128 - oledHeight = 32 -) - -type nonPicoDisplayOutput struct { - bus event.Bus - id hal.HardwareId - width int16 - height int16 -} - -func newNonPicoDisplayOutput(bus event.Bus, id hal.HardwareId) displayProvider { - dp := &nonPicoDisplayOutput{ - bus: bus, - id: id, - width: oledWidth, - height: oledHeight, - } - - return dp -} - -func (d *nonPicoDisplayOutput) ClearBuffer() { - d.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) { - d.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 { - d.bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{ - Op: HwDisplayOpDisplay, - }) - return nil -} - -//============= Init =============// - -func init() { - RevisionMarker = newRevisionMarker() - InputDigital1 = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdDigital1Input)) - InputAnalog1 = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdAnalog1Input)) - OutputDisplay1 = newDisplayOutput(newNonPicoDisplayOutput(DefaultEventBus, hal.HardwareIdDisplay1Output)) - InputButton1 = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdButton1Input)) - InputButton2 = newDigitalInput(newNonPicoDigitalReader(DefaultEventBus, hal.HardwareIdButton2Input)) - InputKnob1 = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdKnob1Input)) - InputKnob2 = newAnalogInput(newNonPicoAdc(DefaultEventBus, hal.HardwareIdKnob2Input)) - OutputVoltage1 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage1Output)) - OutputVoltage2 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage2Output)) - OutputVoltage3 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage3Output)) - OutputVoltage4 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage4Output)) - OutputVoltage5 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage5Output)) - OutputVoltage6 = newVoltageOuput(newNonPicoPwm(DefaultEventBus, hal.HardwareIdVoltage6Output)) - DeviceRandomGenerator1 = newRandomGenerator(nil) -} diff --git a/hardware/rev1/pico.go b/hardware/rev1/pico.go deleted file mode 100644 index 2edc749..0000000 --- a/hardware/rev1/pico.go +++ /dev/null @@ -1,234 +0,0 @@ -//go:build pico -// +build pico - -package rev1 - -import ( - "fmt" - "image/color" - "machine" - "math" - "math/rand" - "runtime/interrupt" - "runtime/volatile" - - "github.com/awonak/EuroPiGo/hardware/hal" - "tinygo.org/x/drivers/ssd1306" -) - -//============= ADC =============// - -type picoAdc struct { - adc machine.ADC -} - -func newPicoAdc(pin machine.Pin) adcProvider { - 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) -} - -//============= DigitalReader =============// - -type picoDigitalReader struct { - pin machine.Pin -} - -func newPicoDigitalReader(pin machine.Pin) digitalReaderProvider { - 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 -} - -//============= PWM =============// - -type picoPwm struct { - pwm pwmGroup - pin machine.Pin - ch uint8 - v uint32 -} - -// 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 -} - -func newPicoPwm(pwm pwmGroup, pin machine.Pin) pwmProvider { - p := &picoPwm{ - pwm: pwm, - pin: pin, - } - return p -} - -func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error { - state := interrupt.Disable() - defer interrupt.Restore(state) - - err := p.pwm.Configure(machine.PWMConfig{ - Period: uint64(config.Period.Nanoseconds()), - }) - if err != nil { - return fmt.Errorf("pwm Configure error: %w", err) - } - - p.pwm.SetTop(uint32(config.Top)) - ch, err := p.pwm.Channel(p.pin) - if err != nil { - return fmt.Errorf("pwm Channel error: %w", err) - } - p.ch = ch - - return nil -} - -func (p *picoPwm) Set(v float32, ofs uint16) { - invertedV := v * float32(p.pwm.Top()) - // volts := (float32(o.pwm.Top()) - invertedCv) - o.ofs - volts := invertedV - float32(ofs) - state := interrupt.Disable() - p.pwm.Set(p.ch, uint32(volts)) - interrupt.Restore(state) - volatile.StoreUint32(&p.v, math.Float32bits(v)) -} - -func (p *picoPwm) Get() float32 { - return math.Float32frombits(volatile.LoadUint32(&p.v)) -} - -//============= Display =============// - -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) displayProvider { - 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() -} - -//============= RND =============// - -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 -} - -//============= Init =============// - -func init() { - machine.InitADC() - - RevisionMarker = newRevisionMarker() - InputDigital1 = newDigitalInput(newPicoDigitalReader(machine.GPIO22)) - InputAnalog1 = newAnalogInput(newPicoAdc(machine.ADC0)) - OutputDisplay1 = newDisplayOutput(newPicoDisplayOutput(machine.I2C0, machine.GPIO0, machine.GPIO1)) - InputButton1 = newDigitalInput(newPicoDigitalReader(machine.GPIO4)) - InputButton2 = newDigitalInput(newPicoDigitalReader(machine.GPIO5)) - InputKnob1 = newAnalogInput(newPicoAdc(machine.ADC1)) - InputKnob2 = newAnalogInput(newPicoAdc(machine.ADC2)) - OutputVoltage1 = newVoltageOuput(newPicoPwm(machine.PWM2, machine.GPIO21)) - OutputVoltage2 = newVoltageOuput(newPicoPwm(machine.PWM2, machine.GPIO20)) - OutputVoltage3 = newVoltageOuput(newPicoPwm(machine.PWM0, machine.GPIO16)) - OutputVoltage4 = newVoltageOuput(newPicoPwm(machine.PWM0, machine.GPIO17)) - OutputVoltage5 = newVoltageOuput(newPicoPwm(machine.PWM1, machine.GPIO18)) - OutputVoltage6 = newVoltageOuput(newPicoPwm(machine.PWM1, machine.GPIO19)) - DeviceRandomGenerator1 = newRandomGenerator(&picoRnd{}) -} diff --git a/hardware/rev1/platform.go b/hardware/rev1/platform.go index 5d78ace..b3f3734 100644 --- a/hardware/rev1/platform.go +++ b/hardware/rev1/platform.go @@ -5,9 +5,8 @@ import ( ) // These will be configured during `init()` from platform-specific files. -// See `pico.go` and `nonpico.go` for more information. +// See `hardware/pico/pico.go` and `hardware/nonpico/nonpico.go` for more information. var ( - RevisionMarker hal.RevisionMarker InputDigital1 hal.DigitalInput InputAnalog1 hal.AnalogInput OutputDisplay1 hal.DisplayOutput @@ -28,9 +27,6 @@ var ( // a `nil` result means that the hardware was not found or some sort of error occurred. func GetHardware[T any](hw hal.HardwareId) T { switch hw { - case hal.HardwareIdRevisionMarker: - t, _ := RevisionMarker.(T) - return t case hal.HardwareIdDigital1Input: t, _ := InputDigital1.(T) return t @@ -78,3 +74,43 @@ func GetHardware[T any](hw hal.HardwareId) T { return none } } + +// Initialize sets up the hardware +// +// This is only to be called by the automatic platform initialization functions +func Initialize(params InitializationParameters) { + InputDigital1 = newDigitalInput(params.InputDigital1) + InputAnalog1 = newAnalogInput(params.InputAnalog1) + OutputDisplay1 = newDisplayOutput(params.OutputDisplay1) + InputButton1 = newDigitalInput(params.InputButton1) + InputButton2 = newDigitalInput(params.InputButton2) + InputKnob1 = newAnalogInput(params.InputKnob1) + InputKnob2 = newAnalogInput(params.InputKnob2) + OutputVoltage1 = newVoltageOuput(params.OutputVoltage1) + OutputVoltage2 = newVoltageOuput(params.OutputVoltage2) + OutputVoltage3 = newVoltageOuput(params.OutputVoltage3) + OutputVoltage4 = newVoltageOuput(params.OutputVoltage4) + OutputVoltage5 = newVoltageOuput(params.OutputVoltage5) + OutputVoltage6 = newVoltageOuput(params.OutputVoltage6) + DeviceRandomGenerator1 = 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 DigitalReaderProvider + InputAnalog1 ADCProvider + OutputDisplay1 DisplayProvider + InputButton1 DigitalReaderProvider + InputButton2 DigitalReaderProvider + InputKnob1 ADCProvider + InputKnob2 ADCProvider + OutputVoltage1 PWMProvider + OutputVoltage2 PWMProvider + OutputVoltage3 PWMProvider + OutputVoltage4 PWMProvider + OutputVoltage5 PWMProvider + OutputVoltage6 PWMProvider + DeviceRandomGenerator1 RNDProvider +} diff --git a/hardware/rev1/randomgenerator.go b/hardware/rev1/randomgenerator.go index 4a00a45..cc01f60 100644 --- a/hardware/rev1/randomgenerator.go +++ b/hardware/rev1/randomgenerator.go @@ -5,7 +5,7 @@ import ( ) type randomGenerator struct { - rnd rndProvider + rnd RNDProvider } var ( @@ -15,13 +15,13 @@ var ( _ = newRandomGenerator ) -func newRandomGenerator(rnd rndProvider) hal.RandomGenerator { +func newRandomGenerator(rnd RNDProvider) hal.RandomGenerator { return &randomGenerator{ rnd: rnd, } } -type rndProvider interface { +type RNDProvider interface { Configure(config hal.RandomGeneratorConfig) error } diff --git a/hardware/rev1/revisionmarker.go b/hardware/rev1/revisionmarker.go deleted file mode 100644 index 98b0e3d..0000000 --- a/hardware/rev1/revisionmarker.go +++ /dev/null @@ -1,21 +0,0 @@ -package rev1 - -import "github.com/awonak/EuroPiGo/hardware/hal" - -type revisionMarker struct{} - -var ( - // static check - _ hal.RevisionMarker = &revisionMarker{} - // silence linter - _ = newRevisionMarker -) - -func newRevisionMarker() hal.RevisionMarker { - return &revisionMarker{} -} - -// Revision returns the detected revision of the current hardware -func (r *revisionMarker) Revision() hal.Revision { - return hal.Revision1 -} diff --git a/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go index 38002e8..5d9bf46 100644 --- a/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -26,7 +26,7 @@ var defaultPeriod time.Duration = time.Nanosecond * 500 // voltageoutput is struct for interacting with the CV/VOct voltage output jacks. type voltageoutput struct { - pwm pwmProvider + pwm PWMProvider ofs uint16 } @@ -37,14 +37,14 @@ var ( _ = newVoltageOuput ) -type pwmProvider interface { +type PWMProvider interface { Configure(config hal.VoltageOutputConfig) error Set(v float32, ofs uint16) Get() float32 } // NewOutput returns a new Output interface. -func newVoltageOuput(pwm pwmProvider) hal.VoltageOutput { +func newVoltageOuput(pwm PWMProvider) hal.VoltageOutput { o := &voltageoutput{ pwm: pwm, } diff --git a/internal/nonpico/nonpico.go b/internal/nonpico/nonpico.go new file mode 100644 index 0000000..90eb4ba --- /dev/null +++ b/internal/nonpico/nonpico.go @@ -0,0 +1,31 @@ +//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/rev1" +) + +func initRevision1() { + rev1.DoInit() +} + +func initRevision2() { + //TODO: rev2.DoInit() +} + +func init() { + hardware.OnRevisionDetected <- func(revision hal.Revision) { + switch revision { + case hal.Revision1: + initRevision1() + case hal.Revision2: + initRevision2() + default: + } + hardware.SetReady() + } +} diff --git a/internal/nonpico/rev1.go b/internal/nonpico/rev1.go new file mode 100644 index 0000000..a868e0f --- /dev/null +++ b/internal/nonpico/rev1.go @@ -0,0 +1,14 @@ +//go:build !pico && (revision1 || europi) +// +build !pico +// +build revision1 europi + +package nonpico + +import ( + "github.com/awonak/EuroPiGo/hardware" + "github.com/awonak/EuroPiGo/hardware/hal" +) + +func init() { + hardware.SetDetectedRevision(hal.Revision1) +} diff --git a/internal/nonpico/rev1/adc.go b/internal/nonpico/rev1/adc.go new file mode 100644 index 0000000..44b4a56 --- /dev/null +++ b/internal/nonpico/rev1/adc.go @@ -0,0 +1,37 @@ +//go:build !pico +// +build !pico + +package rev1 + +import ( + "fmt" + + "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" +) + +type nonPicoAdc struct { + bus event.Bus + id hal.HardwareId + value uint16 +} + +func newNonPicoAdc(bus event.Bus, id hal.HardwareId) rev1.ADCProvider { + adc := &nonPicoAdc{ + bus: bus, + 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/rev1/api.go b/internal/nonpico/rev1/api.go index 22b0ab4..4432cf7 100644 --- a/internal/nonpico/rev1/api.go +++ b/internal/nonpico/rev1/api.go @@ -5,38 +5,37 @@ package rev1 import ( "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" ) -type voltageOutput struct { +type voltageOutputMsg struct { Kind string `json:"kind"` HardwareId hal.HardwareId `json:"hardwareId"` Voltage float32 `json:"voltage"` } // displayMode = displayModeSeparate (0) -type displayOutput struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Op rev1.HwDisplayOp `json:"op"` - Params []int16 `json:"params"` +type displayOutputMsg struct { + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Op HwDisplayOp `json:"op"` + Params []int16 `json:"params"` } // displayMode = displayModeCombined (1) -type displayScreenOuptut struct { +type displayScreenOuptutMsg struct { Kind string `json:"kind"` Width int `json:"width"` Height int `json:"height"` Data []byte `json:"data"` } -type setDigitalInput struct { +type setDigitalInputMsg struct { Kind string `json:"kind"` HardwareId hal.HardwareId `json:"hardwareId"` Value bool `json:"value"` } -type setAnalogInput struct { +type setAnalogInputMsg struct { Kind string `json:"kind"` HardwareId hal.HardwareId `json:"hardwareId"` Voltage float32 `json:"voltage"` diff --git a/internal/nonpico/rev1/digitalreader.go b/internal/nonpico/rev1/digitalreader.go new file mode 100644 index 0000000..44686e5 --- /dev/null +++ b/internal/nonpico/rev1/digitalreader.go @@ -0,0 +1,42 @@ +//go:build !pico +// +build !pico + +package rev1 + +import ( + "fmt" + + "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" +) + +type nonPicoDigitalReader struct { + bus event.Bus + id hal.HardwareId + value bool +} + +func newNonPicoDigitalReader(bus event.Bus, id hal.HardwareId) rev1.DigitalReaderProvider { + dr := &nonPicoDigitalReader{ + bus: bus, + id: id, + } + 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(d.bus, fmt.Sprintf("hw_interrupt_%d", d.id), func(msg HwMessageInterrupt) { + if (msg.Change & changes) != 0 { + handler() + } + }) +} diff --git a/internal/nonpico/rev1/displayoutput.go b/internal/nonpico/rev1/displayoutput.go new file mode 100644 index 0000000..c7a3002 --- /dev/null +++ b/internal/nonpico/rev1/displayoutput.go @@ -0,0 +1,59 @@ +//go:build !pico +// +build !pico + +package rev1 + +import ( + "fmt" + "image/color" + + "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" +) + +const ( + oledWidth = 128 + oledHeight = 32 +) + +type nonPicoDisplayOutput struct { + bus event.Bus + id hal.HardwareId + width int16 + height int16 +} + +func newNonPicoDisplayOutput(bus event.Bus, id hal.HardwareId) rev1.DisplayProvider { + dp := &nonPicoDisplayOutput{ + bus: bus, + id: id, + width: oledWidth, + height: oledHeight, + } + + return dp +} + +func (d *nonPicoDisplayOutput) ClearBuffer() { + d.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) { + d.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 { + d.bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{ + Op: HwDisplayOpDisplay, + }) + return nil +} diff --git a/internal/nonpico/rev1/events/listeners.go b/internal/nonpico/rev1/listeners.go similarity index 54% rename from internal/nonpico/rev1/events/listeners.go rename to internal/nonpico/rev1/listeners.go index 7277253..da1393e 100644 --- a/internal/nonpico/rev1/events/listeners.go +++ b/internal/nonpico/rev1/listeners.go @@ -1,7 +1,7 @@ -//go:build !pico && revision1 -// +build !pico,revision1 +//go:build !pico +// +build !pico -package events +package rev1 import ( "fmt" @@ -15,15 +15,14 @@ import ( ) var ( + bus = event.NewBus() voLerp = lerp.NewLerp32[uint16](0, math.MaxUint16) ) -func SetupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) { - bus := rev1.DefaultEventBus - +func setupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) { for id := hal.HardwareIdVoltage1Output; id <= hal.HardwareIdVoltage6Output; id++ { - fn := func(hid hal.HardwareId) func(rev1.HwMessagePwmValue) { - return func(msg rev1.HwMessagePwmValue) { + fn := func(hid hal.HardwareId) func(HwMessagePwmValue) { + return func(msg HwMessagePwmValue) { v := voLerp.ClampedInverseLerp(msg.Value) * rev1.MaxOutputVoltage cb(hid, v) } @@ -32,10 +31,10 @@ func SetupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) { } } -func SetupDisplayOutputListener(cb func(id hal.HardwareId, op rev1.HwDisplayOp, params []int16)) { - bus := rev1.DefaultEventBus +func setupDisplayOutputListener(cb func(id hal.HardwareId, op HwDisplayOp, params []int16)) { + bus := bus id := hal.HardwareIdDisplay1Output - event.Subscribe(bus, fmt.Sprintf("hw_display_%d", id), func(msg rev1.HwMessageDisplay) { + event.Subscribe(bus, fmt.Sprintf("hw_display_%d", id), func(msg HwMessageDisplay) { cb(id, msg.Op, msg.Operands) }) @@ -45,25 +44,23 @@ var ( states sync.Map ) -func SetDigitalInput(id hal.HardwareId, value bool) { +func setDigitalInput(id hal.HardwareId, value bool) { prevState, _ := states.Load(id) - bus := rev1.DefaultEventBus - states.Store(id, value) - bus.Post(fmt.Sprintf("hw_value_%d", id), rev1.HwMessageDigitalValue{ + bus.Post(fmt.Sprintf("hw_value_%d", id), HwMessageDigitalValue{ Value: value, }) if prevState != value { if value { // rising - bus.Post(fmt.Sprintf("hw_interrupt_%d", id), rev1.HwMessageInterrupt{ + bus.Post(fmt.Sprintf("hw_interrupt_%d", id), HwMessageInterrupt{ Change: hal.ChangeRising, }) } else { // falling - bus.Post(fmt.Sprintf("hw_interrupt_%d", id), rev1.HwMessageInterrupt{ + bus.Post(fmt.Sprintf("hw_interrupt_%d", id), HwMessageInterrupt{ Change: hal.ChangeFalling, }) } @@ -74,10 +71,8 @@ var ( aiLerp = lerp.NewLerp32[uint16](rev1.DefaultCalibratedMinAI, rev1.DefaultCalibratedMaxAI) ) -func SetAnalogInput(id hal.HardwareId, voltage float32) { - bus := rev1.DefaultEventBus - - bus.Post(fmt.Sprintf("hw_value_%d", id), rev1.HwMessageADCValue{ +func setAnalogInput(id hal.HardwareId, voltage float32) { + bus.Post(fmt.Sprintf("hw_value_%d", id), HwMessageADCValue{ Value: aiLerp.Lerp(voltage), }) } diff --git a/hardware/rev1/messages.go b/internal/nonpico/rev1/messages.go similarity index 100% rename from hardware/rev1/messages.go rename to internal/nonpico/rev1/messages.go diff --git a/internal/nonpico/rev1/platform.go b/internal/nonpico/rev1/platform.go new file mode 100644 index 0000000..1f18e1f --- /dev/null +++ b/internal/nonpico/rev1/platform.go @@ -0,0 +1,25 @@ +package rev1 + +import ( + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" +) + +func DoInit() { + rev1.Initialize(rev1.InitializationParameters{ + InputDigital1: newNonPicoDigitalReader(bus, hal.HardwareIdDigital1Input), + InputAnalog1: newNonPicoAdc(bus, hal.HardwareIdAnalog1Input), + OutputDisplay1: newNonPicoDisplayOutput(bus, hal.HardwareIdDisplay1Output), + InputButton1: newNonPicoDigitalReader(bus, hal.HardwareIdButton1Input), + InputButton2: newNonPicoDigitalReader(bus, hal.HardwareIdButton2Input), + InputKnob1: newNonPicoAdc(bus, hal.HardwareIdKnob1Input), + InputKnob2: newNonPicoAdc(bus, hal.HardwareIdKnob2Input), + OutputVoltage1: newNonPicoPwm(bus, hal.HardwareIdVoltage1Output), + OutputVoltage2: newNonPicoPwm(bus, hal.HardwareIdVoltage2Output), + OutputVoltage3: newNonPicoPwm(bus, hal.HardwareIdVoltage3Output), + OutputVoltage4: newNonPicoPwm(bus, hal.HardwareIdVoltage4Output), + OutputVoltage5: newNonPicoPwm(bus, hal.HardwareIdVoltage5Output), + OutputVoltage6: newNonPicoPwm(bus, hal.HardwareIdVoltage6Output), + DeviceRandomGenerator1: nil, + }) +} diff --git a/internal/nonpico/rev1/pwm.go b/internal/nonpico/rev1/pwm.go new file mode 100644 index 0000000..1a366c8 --- /dev/null +++ b/internal/nonpico/rev1/pwm.go @@ -0,0 +1,45 @@ +//go:build !pico +// +build !pico + +package rev1 + +import ( + "fmt" + "math" + + "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" +) + +type nonPicoPwm struct { + bus event.Bus + id hal.HardwareId + v float32 +} + +func newNonPicoPwm(bus event.Bus, id hal.HardwareId) rev1.PWMProvider { + p := &nonPicoPwm{ + bus: bus, + id: id, + } + return p +} + +func (p *nonPicoPwm) Configure(config hal.VoltageOutputConfig) error { + return nil +} + +func (p *nonPicoPwm) Set(v float32, ofs uint16) { + invertedV := v * math.MaxUint16 + // volts := (float32(o.pwm.Top()) - invertedCv) - o.ofs + volts := invertedV - float32(ofs) + p.v = v + p.bus.Post(fmt.Sprintf("hw_pwm_%d", p.id), HwMessagePwmValue{ + Value: uint16(volts), + }) +} + +func (p *nonPicoPwm) Get() float32 { + return p.v +} diff --git a/internal/nonpico/rev1/wsactivation.go b/internal/nonpico/rev1/wsactivation.go index 2fb9b93..7074e72 100644 --- a/internal/nonpico/rev1/wsactivation.go +++ b/internal/nonpico/rev1/wsactivation.go @@ -14,8 +14,6 @@ import ( "strconv" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" - "github.com/awonak/EuroPiGo/internal/nonpico/rev1/events" "github.com/awonak/EuroPiGo/internal/nonpico/ws" ) @@ -79,8 +77,8 @@ func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) { defer sock.Close() - events.SetupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) { - _ = sock.WriteJSON(voltageOutput{ + setupVoltageOutputListeners(func(id hal.HardwareId, voltage float32) { + _ = sock.WriteJSON(voltageOutputMsg{ Kind: "voltageOutput", HardwareId: id, Voltage: voltage, @@ -88,21 +86,21 @@ func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) { }) displayWidth, displayHeight := 128, 32 - displayScreenOutputMsg := displayScreenOuptut{ + displayScreenOutputMsg := displayScreenOuptutMsg{ Kind: "displayScreenOutput", Width: displayWidth, Height: displayHeight, Data: make([]byte, displayWidth*displayHeight*4), } - events.SetupDisplayOutputListener(func(id hal.HardwareId, op rev1.HwDisplayOp, params []int16) { + setupDisplayOutputListener(func(id hal.HardwareId, op HwDisplayOp, params []int16) { switch a.displayMode { case displayModeCombined: switch op { - case rev1.HwDisplayOpClearBuffer: + case HwDisplayOpClearBuffer: for i := range displayScreenOutputMsg.Data { displayScreenOutputMsg.Data[i] = 0 } - case rev1.HwDisplayOpSetPixel: + case HwDisplayOpSetPixel: y, x := int(params[1]), int(params[0]) if y < 0 || y >= displayHeight || x < 0 || x >= displayWidth { break @@ -112,13 +110,13 @@ func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) { displayScreenOutputMsg.Data[pos+1] = byte(params[3]) displayScreenOutputMsg.Data[pos+2] = byte(params[4]) displayScreenOutputMsg.Data[pos+3] = byte(params[5]) - case rev1.HwDisplayOpDisplay: + case HwDisplayOpDisplay: _ = sock.WriteJSON(displayScreenOutputMsg) default: } default: - _ = sock.WriteJSON(displayOutput{ + _ = sock.WriteJSON(displayOutputMsg{ Kind: "displayOutput", HardwareId: id, Op: op, @@ -152,20 +150,20 @@ func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) { switch k.Kind { case "setDigitalInput": - var di setDigitalInput + var di setDigitalInputMsg if err := json.Unmarshal(blob, &di); err != nil { sock.SetError(err) break } - events.SetDigitalInput(di.HardwareId, di.Value) + setDigitalInput(di.HardwareId, di.Value) case "setAnalogInput": - var ai setAnalogInput + var ai setAnalogInputMsg if err := json.Unmarshal(blob, &ai); err != nil { sock.SetError(err) break } - events.SetAnalogInput(ai.HardwareId, ai.Voltage) + setAnalogInput(ai.HardwareId, ai.Voltage) default: // ignore diff --git a/internal/nonpico/rev2.go b/internal/nonpico/rev2.go new file mode 100644 index 0000000..b0a2129 --- /dev/null +++ b/internal/nonpico/rev2.go @@ -0,0 +1,14 @@ +//go:build !pico && (revision2 || europix) +// +build !pico +// +build revision2 europix + +package nonpico + +import ( + "github.com/awonak/EuroPiGo/hardware" + "github.com/awonak/EuroPiGo/hardware/hal" +) + +func init() { + hardware.SetDetectedRevision(hal.Revision2) +} diff --git a/internal/nonpico/ws/websocket.go b/internal/nonpico/ws/websocket.go index bd8fccf..9f85648 100644 --- a/internal/nonpico/ws/websocket.go +++ b/internal/nonpico/ws/websocket.go @@ -1,5 +1,5 @@ -//go:build !pico && revision1 -// +build !pico,revision1 +//go:build !pico +// +build !pico package ws diff --git a/internal/pico/adc.go b/internal/pico/adc.go new file mode 100644 index 0000000..80fdc4a --- /dev/null +++ b/internal/pico/adc.go @@ -0,0 +1,44 @@ +//go:build pico +// +build pico + +package pico + +import ( + "machine" + "runtime/interrupt" + "sync" + + "github.com/awonak/EuroPiGo/hardware/rev1" +) + +var ( + adcOnce sync.Once +) + +type picoAdc struct { + adc machine.ADC +} + +func newPicoAdc(pin machine.Pin) rev1.ADCProvider { + 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..bf06ca1 --- /dev/null +++ b/internal/pico/digitalreader.go @@ -0,0 +1,53 @@ +//go:build pico +// +build pico + +package pico + +import ( + "machine" + "runtime/interrupt" + + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" +) + +type picoDigitalReader struct { + pin machine.Pin +} + +func newPicoDigitalReader(pin machine.Pin) rev1.DigitalReaderProvider { + 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..c2b7418 --- /dev/null +++ b/internal/pico/display.go @@ -0,0 +1,59 @@ +//go:build pico +// +build pico + +package pico + +import ( + "image/color" + "machine" + + "github.com/awonak/EuroPiGo/hardware/rev1" + "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) rev1.DisplayProvider { + 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..95a8a58 --- /dev/null +++ b/internal/pico/pico.go @@ -0,0 +1,50 @@ +//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/rev1" +) + +// EuroPi (original) +func initRevision1() { + rev1.Initialize(rev1.InitializationParameters{ + InputDigital1: newPicoDigitalReader(machine.GPIO22), + InputAnalog1: newPicoAdc(machine.ADC0), + OutputDisplay1: newPicoDisplayOutput(machine.I2C0, machine.GPIO0, machine.GPIO1), + InputButton1: newPicoDigitalReader(machine.GPIO4), + InputButton2: newPicoDigitalReader(machine.GPIO5), + InputKnob1: newPicoAdc(machine.ADC1), + InputKnob2: newPicoAdc(machine.ADC2), + 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.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..f00cd2a --- /dev/null +++ b/internal/pico/pwm.go @@ -0,0 +1,76 @@ +//go:build pico +// +build pico + +package pico + +import ( + "fmt" + "machine" + "math" + "runtime/interrupt" + "runtime/volatile" + + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev1" +) + +type picoPwm struct { + pwm pwmGroup + pin machine.Pin + ch uint8 + v uint32 +} + +// 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 +} + +func newPicoPwm(pwm pwmGroup, pin machine.Pin) rev1.PWMProvider { + p := &picoPwm{ + pwm: pwm, + pin: pin, + } + return p +} + +func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error { + state := interrupt.Disable() + defer interrupt.Restore(state) + + err := p.pwm.Configure(machine.PWMConfig{ + Period: uint64(config.Period.Nanoseconds()), + }) + if err != nil { + return fmt.Errorf("pwm Configure error: %w", err) + } + + p.pwm.SetTop(uint32(config.Top)) + ch, err := p.pwm.Channel(p.pin) + if err != nil { + return fmt.Errorf("pwm Channel error: %w", err) + } + p.ch = ch + + return nil +} + +func (p *picoPwm) Set(v float32, ofs uint16) { + invertedV := v * float32(p.pwm.Top()) + // volts := (float32(o.pwm.Top()) - invertedCv) - o.ofs + volts := invertedV - float32(ofs) + state := interrupt.Disable() + p.pwm.Set(p.ch, uint32(volts)) + interrupt.Restore(state) + volatile.StoreUint32(&p.v, math.Float32bits(v)) +} + +func (p *picoPwm) Get() float32 { + return math.Float32frombits(volatile.LoadUint32(&p.v)) +} diff --git a/internal/pico/revisiondetection.go b/internal/pico/revisiondetection.go new file mode 100644 index 0000000..8a5550d --- /dev/null +++ b/internal/pico/revisiondetection.go @@ -0,0 +1,56 @@ +//go:build pico +// +build pico + +package pico + +import ( + "machine" + "runtime/interrupt" + + "github.com/awonak/EuroPiGo/hardware" + "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 + return hal.Revision1 + case 1: // 0001 + return hal.Revision2 + default: // not yet known or maybe Revision0 / EuroPi-Proto? + return hal.RevisionUnknown + } +} + +func init() { + rev := DetectRevision() + hardware.SetDetectedRevision(rev) +} 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/nonpico.go b/nonpico.go new file mode 100644 index 0000000..056e0b6 --- /dev/null +++ b/nonpico.go @@ -0,0 +1,14 @@ +//go:build !pico +// +build !pico + +package europi + +import ( + _ "github.com/awonak/EuroPiGo/internal/nonpico" +) + +// This file exists to import the non-pico code into the active build +// do not remove this file or remove the init() function below + +func init() { +} diff --git a/pico.go b/pico.go new file mode 100644 index 0000000..a2e23bd --- /dev/null +++ b/pico.go @@ -0,0 +1,14 @@ +//go:build pico +// +build pico + +package europi + +import ( + _ "github.com/awonak/EuroPiGo/internal/pico" +) + +// This file exists to import the pico code into the active build +// do not remove this file or remove the init() function below + +func init() { +} From 663ed67e5144b0b58a689b0981826daef01072d0 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 25 Apr 2023 22:31:31 -0700 Subject: [PATCH 36/62] removed junk --- event/bus_test.go | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/event/bus_test.go b/event/bus_test.go index 9edffe0..31a2219 100644 --- a/event/bus_test.go +++ b/event/bus_test.go @@ -88,49 +88,3 @@ func TestBus(t *testing.T) { }) }) } - -/* -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) - } -} -*/ From 3ccf788e89313e8e0dc5ac77916c89be1b89fcf2 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 25 Apr 2023 23:00:15 -0700 Subject: [PATCH 37/62] Fix readme files. --- README.md | 31 +++++++++++++++++++++++++++++++ hardware/README.md | 10 +++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2fb6e21..28f6a8f 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,37 @@ Ctrl + Shift + P > Tasks: Run Build Task TODO +## 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 this? 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/hardware/README.md b/hardware/README.md index 8574aaa..57983cc 100644 --- a/hardware/README.md +++ b/hardware/README.md @@ -6,11 +6,11 @@ This package is used for obtaining singleton objects for particular hardware, id **NOTE**: The full revision list may be found under [hal/revision.go](hal/revision.go). -| Identifier | Alias | 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` | EuroPi 'production' release, revision 1. | -| `Revision2` | `EuroPiX` | EuroPi X - an improved hardware revision of the EuroPi. Currently in pre-production development hell. | +| 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 From c1de5264d1b19fd4b76747d3165488f54db2e9fd Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Wed, 26 Apr 2023 08:32:15 -0700 Subject: [PATCH 38/62] Small fixes --- europi_test.go | 2 +- experimental/knobbank/knobbank.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/europi_test.go b/europi_test.go index 752f5fa..85f632d 100644 --- a/europi_test.go +++ b/europi_test.go @@ -16,7 +16,7 @@ func TestNew(t *testing.T) { }) t.Run("Revision1", func(t *testing.T) { - if actual := europi.New(hal.Revision1); actual == nil { + if actual := europi.NewFrom(hal.Revision1); actual == nil { t.Fatal("EuroPi New: expected[non-nil] actual[nil]") } }) diff --git a/experimental/knobbank/knobbank.go b/experimental/knobbank/knobbank.go index 709f309..6854f67 100644 --- a/experimental/knobbank/knobbank.go +++ b/experimental/knobbank/knobbank.go @@ -56,11 +56,11 @@ func (kb *KnobBank) Current() hal.KnobInput { } func (kb *KnobBank) MinVoltage() float32 { - return kb.MinVoltage() + return kb.knob.MinVoltage() } func (kb *KnobBank) MaxVoltage() float32 { - return kb.MaxVoltage() + return kb.knob.MaxVoltage() } func (kb *KnobBank) ReadVoltage() float32 { From a2ca2ef8f758564764c30b4e38bec4fbf6bbd977 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Wed, 26 Apr 2023 11:15:21 -0700 Subject: [PATCH 39/62] Clean up lint warnings --- experimental/screenbank/screenbank.go | 1 - internal/projects/clockgenerator/clockgenerator.go | 6 ++++-- internal/projects/clockwerk/clockwerk.go | 12 ++++++------ internal/projects/diagnostics/diagnostics.go | 4 ++-- internal/projects/randomskips/randomskips.go | 6 ++++-- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/experimental/screenbank/screenbank.go b/experimental/screenbank/screenbank.go index 1f33e7c..be04243 100644 --- a/experimental/screenbank/screenbank.go +++ b/experimental/screenbank/screenbank.go @@ -10,7 +10,6 @@ import ( ) type ScreenBank struct { - screen europi.UserInterface current int bank []screenBankEntry writer fontwriter.Writer diff --git a/internal/projects/clockgenerator/clockgenerator.go b/internal/projects/clockgenerator/clockgenerator.go index 0df3d9a..e95aeb9 100644 --- a/internal/projects/clockgenerator/clockgenerator.go +++ b/internal/projects/clockgenerator/clockgenerator.go @@ -54,7 +54,7 @@ func main() { // some options shown below are being explicitly set to their defaults // only to showcase their existence. - europi.Bootstrap( + if err := europi.Bootstrap( europi.EnableDisplayLogger(false), europi.InitRandom(true), europi.StartLoop(startLoop), @@ -62,5 +62,7 @@ func main() { europi.MainLoopInterval(time.Millisecond*1), europi.UI(ui), europi.UIRefreshRate(time.Millisecond*50), - ) + ); err != nil { + panic(err) + } } diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index 6d6f993..90d1c0a 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -164,11 +164,11 @@ func (c *Clockwerk) clock(i uint8, reset chan uint8) { c.CV[i].SetCV(1.0) t = t.Add(high) - time.Sleep(t.Sub(time.Now())) + time.Sleep(time.Since(t)) c.CV[i].SetCV(0.0) t = t.Add(low) - time.Sleep(t.Sub(time.Now())) + time.Sleep(time.Since(t)) } } @@ -220,9 +220,9 @@ func (c *Clockwerk) updateDisplay() { xWidth := int16(divWidth) xOffset := int16(c.selected) * xWidth // TODO: replace box with chevron. - tinydraw.Rectangle(c.Display, xOffset, 16, xWidth, 16, draw.White) + _ = tinydraw.Rectangle(c.Display, xOffset, 16, xWidth, 16, draw.White) - c.Display.Display() + _ = c.Display.Display() } var app Clockwerk @@ -239,10 +239,10 @@ func startLoop(e *europi.EuroPi) { app.factorLerp = lerp.NewLerp32(0, len(FactorChoices)-1) // Lower range value can have lower sample size - app.K1.Configure(hal.AnalogInputConfig{ + _ = app.K1.Configure(hal.AnalogInputConfig{ Samples: 500, }) - app.K2.Configure(hal.AnalogInputConfig{ + _ = app.K2.Configure(hal.AnalogInputConfig{ Samples: 20, }) diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index 7b9d1c4..facc6ce 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -44,7 +44,7 @@ func mainLoop(e *europi.EuroPi) { e.Display.ClearBuffer() // Highlight the border of the oled display. - tinydraw.Rectangle(e.Display, 0, 0, 128, 32, draw.White) + _ = tinydraw.Rectangle(e.Display, 0, 0, 128, 32, draw.White) writer := fontwriter.Writer{ Display: e.Display, @@ -67,7 +67,7 @@ func mainLoop(e *europi.EuroPi) { // Show current button press state. writer.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", e.B1.Value(), e.B2.Value()), 3, 28, draw.White) - e.Display.Display() + _ = e.Display.Display() // Set voltage values for the 6 CV outputs. if kv := uint16(e.K1.Percent() * float32(1<<12)); kv != myApp.prevK1 { diff --git a/internal/projects/randomskips/randomskips.go b/internal/projects/randomskips/randomskips.go index 377cc9e..efea09a 100644 --- a/internal/projects/randomskips/randomskips.go +++ b/internal/projects/randomskips/randomskips.go @@ -78,7 +78,7 @@ func main() { // some options shown below are being explicitly set to their defaults // only to showcase their existence. - europi.Bootstrap( + if err := europi.Bootstrap( europi.EnableDisplayLogger(false), europi.InitRandom(true), europi.StartLoop(startLoop), @@ -86,5 +86,7 @@ func main() { europi.MainLoopInterval(time.Millisecond*1), europi.UI(ui), europi.UIRefreshRate(time.Millisecond*50), - ) + ); err != nil { + panic(err) + } } From 84617fb58acfd3aba8c65549fc7d076fa7979b65 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Wed, 26 Apr 2023 11:15:42 -0700 Subject: [PATCH 40/62] Fix issue with CV6 being incorrectly CV5 --- europi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/europi.go b/europi.go index 3c2563c..7950f51 100644 --- a/europi.go +++ b/europi.go @@ -69,7 +69,7 @@ func NewFrom(revision hal.Revision) *EuroPi { CV3: cv3, CV4: cv4, CV5: cv5, - CV6: cv5, + CV6: cv6, CV: [6]hal.VoltageOutput{cv1, cv2, cv3, cv4, cv5, cv6}, RND: hardware.GetHardware[hal.RandomGenerator](revision, hal.HardwareIdRandom1Generator), } From 521ba39fbf9ee2df858d54be27c65aac4a952b88 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Wed, 26 Apr 2023 20:21:33 -0700 Subject: [PATCH 41/62] Update README.md Co-authored-by: Adam Wonak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28f6a8f..c6515e2 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,6 @@ 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 this? +## 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! From f25258e38c2aaa8a075cd6dac7e1380f29e5469d Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Wed, 26 Apr 2023 21:04:24 -0700 Subject: [PATCH 42/62] Update bootstrap_panicdisabled.go europi proto doesn't have a screen, so can't display panic message. Co-authored-by: Adam Wonak --- bootstrap_panicdisabled.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap_panicdisabled.go b/bootstrap_panicdisabled.go index 92b8eba..6be5ff3 100644 --- a/bootstrap_panicdisabled.go +++ b/bootstrap_panicdisabled.go @@ -11,7 +11,7 @@ import ( func init() { hardware.OnRevisionDetected <- func(revision hal.Revision) { switch revision { - case hal.RevisionUnknown: + case hal.RevisionUnknown, hal.EuroPiProto: DefaultPanicHandler = handlePanicLogger default: DefaultPanicHandler = handlePanicDisplayCrash From 8f8c2ae4f1f8ba18475eba9a6829bea47fb06f78 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Wed, 26 Apr 2023 21:36:08 -0700 Subject: [PATCH 43/62] Fixes for PR concerns - rename things to be more clear - document undocumented calls - add TODO notes where necessary - fix an issue where the color Black wasn't. - mea culpa. --- bootstrap.go | 28 ++++----- bootstrap_uimodule.go | 9 ++- bootstrapoptions.go | 8 +-- bootstrapoptions_features.go | 8 +-- bootstrapoptions_lifecycle.go | 63 +++++++++++++------ experimental/draw/colors.go | 2 +- .../projects/clockgenerator/clockgenerator.go | 8 +-- internal/projects/randomskips/randomskips.go | 8 +-- 8 files changed, 82 insertions(+), 52 deletions(-) diff --git a/bootstrap.go b/bootstrap.go index fc71449..ff8de36 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -16,7 +16,7 @@ var ( // Bootstrap will set up a global runtime environment (see europi.Pi) func Bootstrap(options ...BootstrapOption) error { config := bootstrapConfig{ - mainLoopInterval: DefaultMainLoopInterval, + appMainLoopInterval: DefaultAppMainLoopInterval, panicHandler: DefaultPanicHandler, enableDisplayLogger: DefaultEnableDisplayLogger, initRandom: DefaultInitRandom, @@ -26,9 +26,9 @@ func Bootstrap(options ...BootstrapOption) error { onPreInitializeComponentsFn: nil, onPostInitializeComponentsFn: nil, onBootstrapCompletedFn: DefaultBootstrapCompleted, - onStartLoopFn: nil, - onMainLoopFn: DefaultMainLoop, - onEndLoopFn: nil, + onAppStartFn: nil, + onAppMainLoopFn: DefaultMainLoop, + onAppEndFn: nil, onBeginDestroyFn: nil, onFinishDestroyFn: nil, } @@ -128,31 +128,31 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) nonPicoWS } func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { - if config.onStartLoopFn != nil { - config.onStartLoopFn(e) + if config.onAppStartFn != nil { + config.onAppStartFn(e) } startUI(e) ForceRepaintUI(e) - if config.mainLoopInterval > 0 { + if config.appMainLoopInterval > 0 { bootstrapRunLoopWithDelay(config, e) } else { bootstrapRunLoopNoDelay(config, e) } - if config.onEndLoopFn != nil { - config.onEndLoopFn(e) + if config.onAppEndFn != nil { + config.onAppEndFn(e) } } func bootstrapRunLoopWithDelay(config *bootstrapConfig, e *EuroPi) { - if config.onMainLoopFn == nil { + if config.onAppMainLoopFn == nil { panic(errors.New("no main loop specified")) } - ticker := time.NewTicker(config.mainLoopInterval) + ticker := time.NewTicker(config.appMainLoopInterval) defer ticker.Stop() lastTick := time.Now() @@ -162,14 +162,14 @@ func bootstrapRunLoopWithDelay(config *bootstrapConfig, e *EuroPi) { panic(reason) case now := <-ticker.C: - config.onMainLoopFn(e, now.Sub(lastTick)) + config.onAppMainLoopFn(e, now.Sub(lastTick)) lastTick = now } } } func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { - if config.onMainLoopFn == nil { + if config.onAppMainLoopFn == nil { panic(errors.New("no main loop specified")) } @@ -181,7 +181,7 @@ func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { default: now := time.Now() - config.onMainLoopFn(e, now.Sub(lastTick)) + config.onAppMainLoopFn(e, now.Sub(lastTick)) lastTick = now } } diff --git a/bootstrap_uimodule.go b/bootstrap_uimodule.go index 9c9b571..29cc32e 100644 --- a/bootstrap_uimodule.go +++ b/bootstrap_uimodule.go @@ -9,6 +9,11 @@ import ( "github.com/awonak/EuroPiGo/hardware/hal" ) +// LongPressDuration is the amount of time a button is in a held/pressed state before +// it is considered to be a 'long' press. +// TODO: This is eventually intended to be a persisted setting, configurable by the user. +const LongPressDuration = time.Millisecond * 650 + type uiModule struct { screen UserInterface logoPainter UserInterfaceLogoPainter @@ -172,12 +177,10 @@ func (u *uiModule) setupButton(e *EuroPi, btn hal.ButtonInput, onShort func(e *E } } - const longDuration = time.Millisecond * 650 - btn.HandlerEx(hal.ChangeAny, func(value bool, deltaTime time.Duration) { if value { onShort(e, value, deltaTime) - } else if deltaTime < longDuration { + } else if deltaTime < LongPressDuration { onShort(e, value, deltaTime) } else { onLong(e, deltaTime) diff --git a/bootstrapoptions.go b/bootstrapoptions.go index 292baba..15c863b 100644 --- a/bootstrapoptions.go +++ b/bootstrapoptions.go @@ -8,7 +8,7 @@ import ( type BootstrapOption func(o *bootstrapConfig) error type bootstrapConfig struct { - mainLoopInterval time.Duration + appMainLoopInterval time.Duration panicHandler func(e *EuroPi, reason any) enableDisplayLogger bool initRandom bool @@ -24,9 +24,9 @@ type bootstrapConfig struct { onPreInitializeComponentsFn PreInitializeComponentsFunc onPostInitializeComponentsFn PostInitializeComponentsFunc onBootstrapCompletedFn BootstrapCompletedFunc - onStartLoopFn StartLoopFunc - onMainLoopFn MainLoopFunc - onEndLoopFn EndLoopFunc + onAppStartFn AppStartFunc + onAppMainLoopFn AppMainLoopFunc + onAppEndFn AppEndFunc onBeginDestroyFn BeginDestroyFunc onFinishDestroyFn FinishDestroyFunc } diff --git a/bootstrapoptions_features.go b/bootstrapoptions_features.go index debd7e0..3de052b 100644 --- a/bootstrapoptions_features.go +++ b/bootstrapoptions_features.go @@ -6,16 +6,16 @@ import ( ) const ( - DefaultMainLoopInterval time.Duration = time.Millisecond * 100 + DefaultAppMainLoopInterval time.Duration = time.Millisecond * 100 ) -// MainLoopInterval sets the interval between calls to the configured main loop function -func MainLoopInterval(interval time.Duration) BootstrapOption { +// AppMainLoopInterval sets the interval between calls to the configured app main loop function +func AppMainLoopInterval(interval time.Duration) BootstrapOption { return func(o *bootstrapConfig) error { if interval < 0 { return errors.New("interval must be greater than or equal to 0") } - o.mainLoopInterval = interval + o.appMainLoopInterval = interval return nil } } diff --git a/bootstrapoptions_lifecycle.go b/bootstrapoptions_lifecycle.go index 0d4c33f..1f77cea 100644 --- a/bootstrapoptions_lifecycle.go +++ b/bootstrapoptions_lifecycle.go @@ -30,18 +30,18 @@ Callback: BootstrapCompleted Bootstrap: runLoop | V - Callback: StartLoop + Callback: AppStart | V - Callback(on tick): MainLoop + Callback(on tick): AppMainLoop | V - Callback: EndLoop + Callback: AppEnd | V Bootstrap: destroyBootstrap - | - V + | + V Callback: BeginDestroy | V @@ -53,15 +53,16 @@ type ( PreInitializeComponentsFunc func(e *EuroPi) PostInitializeComponentsFunc func(e *EuroPi) BootstrapCompletedFunc func(e *EuroPi) - StartLoopFunc func(e *EuroPi) - MainLoopFunc func(e *EuroPi, deltaTime time.Duration) - EndLoopFunc func(e *EuroPi) + AppStartFunc func(e *EuroPi) + AppMainLoopFunc func(e *EuroPi, deltaTime time.Duration) + AppEndFunc func(e *EuroPi) BeginDestroyFunc func(e *EuroPi, reason any) FinishDestroyFunc func(e *EuroPi) ) -// PostBootstrapConstruction runs immediately after primary EuroPi bootstrap has finished, -// but before components have been initialized +// PostBootstrapConstruction sets the function that runs immediately after primary EuroPi bootstrap +// has finished, but before components have been initialized. Nearly none of the functionality of +// the bootstrap is ready or configured at this point. func PostBootstrapConstruction(fn PostBootstrapConstructionFunc) BootstrapOption { return func(o *bootstrapConfig) error { o.onPostBootstrapConstructionFn = fn @@ -69,6 +70,9 @@ func PostBootstrapConstruction(fn PostBootstrapConstructionFunc) BootstrapOption } } +// PreInitializeComponents sets the function that recevies notification of when components of the +// bootstrap are about to start their initialization phase and the bootstrap is getting ready. +// Most operational functionality of the bootstrap is definitely not configured at this point. func PreInitializeComponents(fn PreInitializeComponentsFunc) BootstrapOption { return func(o *bootstrapConfig) error { o.onPreInitializeComponentsFn = fn @@ -76,6 +80,9 @@ func PreInitializeComponents(fn PreInitializeComponentsFunc) BootstrapOption { } } +// PostInitializeComponents sets the function that recevies notification of when components of the +// bootstrap have completed their initialization phase and the bootstrap is nearly ready for full +// operation. Some operational functionality of the bootstrap might not be configured at this point. func PostInitializeComponents(fn PostInitializeComponentsFunc) BootstrapOption { return func(o *bootstrapConfig) error { o.onPostInitializeComponentsFn = fn @@ -83,6 +90,9 @@ func PostInitializeComponents(fn PostInitializeComponentsFunc) BootstrapOption { } } +// BootstrapCompleted sets the function that receives notification of critical bootstrap +// operations being complete - this is the first point where functions within the bootstrap +// may be used without fear of there being an incomplete operating state. func BootstrapCompleted(fn BootstrapCompletedFunc) BootstrapOption { return func(o *bootstrapConfig) error { o.onBootstrapCompletedFn = fn @@ -90,33 +100,48 @@ func BootstrapCompleted(fn BootstrapCompletedFunc) BootstrapOption { } } -func StartLoop(fn StartLoopFunc) BootstrapOption { +// TODO: consider secondary bootloader support functionality here once internal flash support +// becomes a reality. + +// AppStart sets the application function to be called before the main operating loop +// processing begins. At this point, the bootstrap configuration has completed and +// all bootstrap functionality may be used without fear of there being an incomplete +// operating state. +func AppStart(fn AppStartFunc) BootstrapOption { return func(o *bootstrapConfig) error { - o.onStartLoopFn = fn + o.onAppStartFn = fn return nil } } -// MainLoop sets the main loop function to be called on interval. -// nil is not allowed - if you want to set the default, either do not specify a MainLoop() option +// AppMainLoop sets the application main loop function to be called on interval. +// nil is not allowed - if you want to set the default, either do not specify a AppMainLoop() option // or specify europi.DefaultMainLoop -func MainLoop(fn MainLoopFunc) BootstrapOption { +func AppMainLoop(fn AppMainLoopFunc) BootstrapOption { return func(o *bootstrapConfig) error { if fn == nil { return errors.New("a valid main loop function must be specified") } - o.onMainLoopFn = fn + o.onAppMainLoopFn = fn return nil } } -func EndLoop(fn EndLoopFunc) BootstrapOption { +// AppEnd sets the application function that's called right before the bootstrap +// destruction processing is performed. +func AppEnd(fn AppEndFunc) BootstrapOption { return func(o *bootstrapConfig) error { - o.onEndLoopFn = fn + o.onAppEndFn = fn return nil } } +// BeginDestroy sets the function that receives the notification of shutdown of the bootstrap and +// is also the first stop within the `panic()` handler functionality. If the `reason` parameter +// is non-nil, then a critical failure has been detected and the bootstrap is in the last stages of +// complete destruction. If it is nil, then it can be assumed that proper functionality of the +// bootstrap is still available, but heading towards the last steps of unavailability once the +// function exits. func BeginDestroy(fn BeginDestroyFunc) BootstrapOption { return func(o *bootstrapConfig) error { o.onBeginDestroyFn = fn @@ -124,6 +149,8 @@ func BeginDestroy(fn BeginDestroyFunc) BootstrapOption { } } +// FinishDestroy sets the function that receives the final notification of shutdown of the bootstrap. +// The entire bootstrap is disabled, all timers, queues, and components are considered deactivated. func FinishDestroy(fn FinishDestroyFunc) BootstrapOption { return func(o *bootstrapConfig) error { o.onFinishDestroyFn = fn diff --git a/experimental/draw/colors.go b/experimental/draw/colors.go index 08859c5..e14e13a 100644 --- a/experimental/draw/colors.go +++ b/experimental/draw/colors.go @@ -4,5 +4,5 @@ import "image/color" var ( White = color.RGBA{255, 255, 255, 255} - Black = color.RGBA{255, 255, 255, 255} + Black = color.RGBA{0, 0, 0, 255} ) diff --git a/internal/projects/clockgenerator/clockgenerator.go b/internal/projects/clockgenerator/clockgenerator.go index e95aeb9..859ef27 100644 --- a/internal/projects/clockgenerator/clockgenerator.go +++ b/internal/projects/clockgenerator/clockgenerator.go @@ -20,7 +20,7 @@ var ( } ) -func startLoop(e *europi.EuroPi) { +func appStart(e *europi.EuroPi) { if err := clock.Init(module.Config{ BPM: 120.0, GateDuration: time.Millisecond * 100, @@ -57,9 +57,9 @@ func main() { if err := europi.Bootstrap( europi.EnableDisplayLogger(false), europi.InitRandom(true), - europi.StartLoop(startLoop), - europi.MainLoop(mainLoop), - europi.MainLoopInterval(time.Millisecond*1), + europi.AppStart(appStart), + europi.AppMainLoop(mainLoop), + europi.AppMainLoopInterval(time.Millisecond*1), europi.UI(ui), europi.UIRefreshRate(time.Millisecond*50), ); err != nil { diff --git a/internal/projects/randomskips/randomskips.go b/internal/projects/randomskips/randomskips.go index efea09a..b4a0a97 100644 --- a/internal/projects/randomskips/randomskips.go +++ b/internal/projects/randomskips/randomskips.go @@ -39,7 +39,7 @@ func makeGate(out hal.VoltageOutput) func(value bool) { } } -func startLoop(e *europi.EuroPi) { +func appStart(e *europi.EuroPi) { if err := skip.Init(module.Config{ Gate: makeGate(e.CV1), Chance: 0.5, @@ -81,9 +81,9 @@ func main() { if err := europi.Bootstrap( europi.EnableDisplayLogger(false), europi.InitRandom(true), - europi.StartLoop(startLoop), - europi.MainLoop(mainLoop), - europi.MainLoopInterval(time.Millisecond*1), + europi.AppStart(appStart), + europi.AppMainLoop(mainLoop), + europi.AppMainLoopInterval(time.Millisecond*1), europi.UI(ui), europi.UIRefreshRate(time.Millisecond*50), ); err != nil { From 1cb05491a9962ea071ab69969d206bd21141b88e Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 27 Apr 2023 13:10:34 -0700 Subject: [PATCH 44/62] Clean up some bootstrap options --- bootstrap.go | 64 +++++++++---- bootstrap_ui.go | 6 +- bootstrapoptions.go | 14 +-- bootstrapoptions_app.go | 82 ++++++++++++++++ bootstrapoptions_features.go | 16 ---- bootstrapoptions_lifecycle.go | 6 +- bootstrapoptions_ui.go | 27 +++++- .../projects/clockgenerator/clockgenerator.go | 65 ++++++++----- internal/projects/clockwerk/clockwerk.go | 4 +- internal/projects/diagnostics/diagnostics.go | 4 +- internal/projects/randomskips/randomskips.go | 95 ++++++++++++------- 11 files changed, 265 insertions(+), 118 deletions(-) create mode 100644 bootstrapoptions_app.go diff --git a/bootstrap.go b/bootstrap.go index ff8de36..5aafe52 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -16,29 +16,53 @@ var ( // Bootstrap will set up a global runtime environment (see europi.Pi) func Bootstrap(options ...BootstrapOption) error { config := bootstrapConfig{ - appMainLoopInterval: DefaultAppMainLoopInterval, - panicHandler: DefaultPanicHandler, - enableDisplayLogger: DefaultEnableDisplayLogger, - initRandom: DefaultInitRandom, - europi: nil, + panicHandler: DefaultPanicHandler, + enableDisplayLogger: DefaultEnableDisplayLogger, + initRandom: DefaultInitRandom, + enableNonPicoWebSocket: false, + europi: nil, + + appConfig: bootstrapAppConfig{ + mainLoopInterval: DefaultAppMainLoopInterval, + onAppStartFn: nil, + onAppMainLoopFn: DefaultMainLoop, + onAppEndFn: nil, + }, + + uiConfig: bootstrapUIConfig{ + ui: nil, + uiRefreshRate: DefaultUIRefreshRate, + }, onPostBootstrapConstructionFn: DefaultPostBootstrapInitialization, onPreInitializeComponentsFn: nil, onPostInitializeComponentsFn: nil, onBootstrapCompletedFn: DefaultBootstrapCompleted, - onAppStartFn: nil, - onAppMainLoopFn: DefaultMainLoop, - onAppEndFn: nil, onBeginDestroyFn: nil, onFinishDestroyFn: nil, } + // process bootstrap options for _, opt := range options { if err := opt(&config); err != nil { return err } } + // process app options + for _, opt := range config.appConfig.options { + if err := opt(&config.appConfig); err != nil { + return err + } + } + + // process ui options + for _, opt := range config.uiConfig.options { + if err := opt(&config.uiConfig); err != nil { + return err + } + } + if config.europi == nil { config.europi = New() } @@ -116,8 +140,8 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) nonPicoWS } // ui initializaiton is always last - if config.ui != nil { - enableUI(e, config.ui, config.uiRefreshRate) + if config.uiConfig.ui != nil { + enableUI(e, config.uiConfig) } if config.onPostInitializeComponentsFn != nil { @@ -128,31 +152,31 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) nonPicoWS } func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { - if config.onAppStartFn != nil { - config.onAppStartFn(e) + if config.appConfig.onAppStartFn != nil { + config.appConfig.onAppStartFn(e) } startUI(e) ForceRepaintUI(e) - if config.appMainLoopInterval > 0 { + if config.appConfig.mainLoopInterval > 0 { bootstrapRunLoopWithDelay(config, e) } else { bootstrapRunLoopNoDelay(config, e) } - if config.onAppEndFn != nil { - config.onAppEndFn(e) + if config.appConfig.onAppEndFn != nil { + config.appConfig.onAppEndFn(e) } } func bootstrapRunLoopWithDelay(config *bootstrapConfig, e *EuroPi) { - if config.onAppMainLoopFn == nil { + if config.appConfig.onAppMainLoopFn == nil { panic(errors.New("no main loop specified")) } - ticker := time.NewTicker(config.appMainLoopInterval) + ticker := time.NewTicker(config.appConfig.mainLoopInterval) defer ticker.Stop() lastTick := time.Now() @@ -162,14 +186,14 @@ func bootstrapRunLoopWithDelay(config *bootstrapConfig, e *EuroPi) { panic(reason) case now := <-ticker.C: - config.onAppMainLoopFn(e, now.Sub(lastTick)) + config.appConfig.onAppMainLoopFn(e, now.Sub(lastTick)) lastTick = now } } } func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { - if config.onAppMainLoopFn == nil { + if config.appConfig.onAppMainLoopFn == nil { panic(errors.New("no main loop specified")) } @@ -181,7 +205,7 @@ func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { default: now := time.Now() - config.onAppMainLoopFn(e, now.Sub(lastTick)) + config.appConfig.onAppMainLoopFn(e, now.Sub(lastTick)) lastTick = now } } diff --git a/bootstrap_ui.go b/bootstrap_ui.go index fec4dd3..c4d861c 100644 --- a/bootstrap_ui.go +++ b/bootstrap_ui.go @@ -49,10 +49,10 @@ var ( ui uiModule ) -func enableUI(e *EuroPi, screen UserInterface, interval time.Duration) { - ui.setup(e, screen) +func enableUI(e *EuroPi, config bootstrapUIConfig) { + ui.setup(e, config.ui) - ui.start(e, interval) + ui.start(e, config.uiRefreshRate) } func startUI(e *EuroPi) { diff --git a/bootstrapoptions.go b/bootstrapoptions.go index 15c863b..f0d5718 100644 --- a/bootstrapoptions.go +++ b/bootstrapoptions.go @@ -1,32 +1,26 @@ package europi -import ( - "time" -) - // BootstrapOption is a single configuration parameter passed to the Bootstrap() function type BootstrapOption func(o *bootstrapConfig) error type bootstrapConfig struct { - appMainLoopInterval time.Duration panicHandler func(e *EuroPi, reason any) enableDisplayLogger bool initRandom bool europi *EuroPi enableNonPicoWebSocket bool + // application + appConfig bootstrapAppConfig + // user interface - ui UserInterface - uiRefreshRate time.Duration + uiConfig bootstrapUIConfig // lifecycle callbacks onPostBootstrapConstructionFn PostBootstrapConstructionFunc onPreInitializeComponentsFn PreInitializeComponentsFunc onPostInitializeComponentsFn PostInitializeComponentsFunc onBootstrapCompletedFn BootstrapCompletedFunc - onAppStartFn AppStartFunc - onAppMainLoopFn AppMainLoopFunc - onAppEndFn AppEndFunc onBeginDestroyFn BeginDestroyFunc onFinishDestroyFn FinishDestroyFunc } diff --git a/bootstrapoptions_app.go b/bootstrapoptions_app.go new file mode 100644 index 0000000..4b1a3ec --- /dev/null +++ b/bootstrapoptions_app.go @@ -0,0 +1,82 @@ +package europi + +import ( + "errors" + "time" +) + +type ApplicationStart interface { + Start(e *EuroPi) +} + +type ApplicationMainLoop interface { + MainLoop(e *EuroPi, deltaTime time.Duration) +} + +type ApplicationEnd interface { + End(e *EuroPi) +} + +// App sets the application handler interface with optional parameters +func App(app any, opts ...BootstrapAppOption) BootstrapOption { + return func(o *bootstrapConfig) error { + if app == nil { + return errors.New("app must not be nil") + } + start, _ := app.(ApplicationStart) + mainLoop, _ := app.(ApplicationMainLoop) + end, _ := app.(ApplicationEnd) + + if start == nil && mainLoop == nil && end == nil { + return errors.New("app must provide at least one application function interface (ApplicationStart, ApplicationMainLoop, ApplicationEnd)") + } + + if start != nil { + o.appConfig.onAppStartFn = start.Start + } + if mainLoop != nil { + o.appConfig.onAppMainLoopFn = mainLoop.MainLoop + } + if end != nil { + o.appConfig.onAppEndFn = end.End + } + + o.appConfig.options = opts + return nil + } +} + +// AppOptions adds optional parameters for setting up the application interface +func AppOptions(option BootstrapAppOption, opts ...BootstrapAppOption) BootstrapOption { + return func(o *bootstrapConfig) error { + o.appConfig.options = append(o.appConfig.options, opts...) + return nil + } +} + +// BootstrapAppOption is a single configuration parameter passed to the App() or AppOption() functions +type BootstrapAppOption func(o *bootstrapAppConfig) error + +type bootstrapAppConfig struct { + mainLoopInterval time.Duration + onAppStartFn AppStartFunc + onAppMainLoopFn AppMainLoopFunc + onAppEndFn AppEndFunc + + options []BootstrapAppOption +} + +const ( + DefaultAppMainLoopInterval time.Duration = time.Millisecond * 100 +) + +// AppMainLoopInterval sets the interval between calls to the configured app main loop function +func AppMainLoopInterval(interval time.Duration) BootstrapAppOption { + return func(o *bootstrapAppConfig) error { + if interval < 0 { + return errors.New("interval must be greater than or equal to 0") + } + o.mainLoopInterval = interval + return nil + } +} diff --git a/bootstrapoptions_features.go b/bootstrapoptions_features.go index 3de052b..8e88268 100644 --- a/bootstrapoptions_features.go +++ b/bootstrapoptions_features.go @@ -2,24 +2,8 @@ package europi import ( "errors" - "time" ) -const ( - DefaultAppMainLoopInterval time.Duration = time.Millisecond * 100 -) - -// AppMainLoopInterval sets the interval between calls to the configured app main loop function -func AppMainLoopInterval(interval time.Duration) BootstrapOption { - return func(o *bootstrapConfig) error { - if interval < 0 { - return errors.New("interval must be greater than or equal to 0") - } - o.appMainLoopInterval = interval - return nil - } -} - const ( DefaultEnableDisplayLogger bool = false ) diff --git a/bootstrapoptions_lifecycle.go b/bootstrapoptions_lifecycle.go index 1f77cea..401e8fc 100644 --- a/bootstrapoptions_lifecycle.go +++ b/bootstrapoptions_lifecycle.go @@ -109,7 +109,7 @@ func BootstrapCompleted(fn BootstrapCompletedFunc) BootstrapOption { // operating state. func AppStart(fn AppStartFunc) BootstrapOption { return func(o *bootstrapConfig) error { - o.onAppStartFn = fn + o.appConfig.onAppStartFn = fn return nil } } @@ -122,7 +122,7 @@ func AppMainLoop(fn AppMainLoopFunc) BootstrapOption { if fn == nil { return errors.New("a valid main loop function must be specified") } - o.onAppMainLoopFn = fn + o.appConfig.onAppMainLoopFn = fn return nil } } @@ -131,7 +131,7 @@ func AppMainLoop(fn AppMainLoopFunc) BootstrapOption { // destruction processing is performed. func AppEnd(fn AppEndFunc) BootstrapOption { return func(o *bootstrapConfig) error { - o.onAppEndFn = fn + o.appConfig.onAppEndFn = fn return nil } } diff --git a/bootstrapoptions_ui.go b/bootstrapoptions_ui.go index 154f258..aa40a5e 100644 --- a/bootstrapoptions_ui.go +++ b/bootstrapoptions_ui.go @@ -6,12 +6,13 @@ import ( ) // UI sets the user interface handler interface -func UI(ui UserInterface) BootstrapOption { +func UI(ui UserInterface, opts ...BootstrapUIOption) BootstrapOption { return func(o *bootstrapConfig) error { if ui == nil { return errors.New("ui must not be nil") } - o.ui = ui + o.uiConfig.ui = ui + o.uiConfig.options = opts return nil } } @@ -20,9 +21,27 @@ const ( DefaultUIRefreshRate time.Duration = time.Millisecond * 100 ) -// UIRefreshRate sets the interval of refreshes of the user interface -func UIRefreshRate(interval time.Duration) BootstrapOption { +// BootstrapOption is a single configuration parameter passed to the Bootstrap() function +type BootstrapUIOption func(o *bootstrapUIConfig) error + +type bootstrapUIConfig struct { + ui UserInterface + uiRefreshRate time.Duration + + options []BootstrapUIOption +} + +// UIOptions adds optional parameters for setting up the user interface +func UIOptions(option BootstrapUIOption, opts ...BootstrapUIOption) BootstrapOption { return func(o *bootstrapConfig) error { + o.uiConfig.options = append(o.uiConfig.options, opts...) + return nil + } +} + +// UIRefreshRate sets the interval of refreshes of the user interface +func UIRefreshRate(interval time.Duration) BootstrapUIOption { + return func(o *bootstrapUIConfig) error { if interval <= 0 { return errors.New("interval must be greater than 0") } diff --git a/internal/projects/clockgenerator/clockgenerator.go b/internal/projects/clockgenerator/clockgenerator.go index 859ef27..4809c49 100644 --- a/internal/projects/clockgenerator/clockgenerator.go +++ b/internal/projects/clockgenerator/clockgenerator.go @@ -9,19 +9,41 @@ import ( "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" ) -var ( - clock module.ClockGenerator - ui *screenbank.ScreenBank - screenMain = screen.Main{ - Clock: &clock, +type application struct { + clock *module.ClockGenerator + + ui *screenbank.ScreenBank + screenMain screen.Main + screenSettings screen.Settings +} + +func newApplication() (*application, error) { + clock := &module.ClockGenerator{} + app := &application{ + clock: clock, + + screenMain: screen.Main{ + Clock: clock, + }, + screenSettings: screen.Settings{ + Clock: clock, + }, } - screenSettings = screen.Settings{ - Clock: &clock, + + var err error + app.ui, err = screenbank.NewScreenBank( + screenbank.WithScreen("main", "\u2b50", &app.screenMain), + screenbank.WithScreen("settings", "\u2611", &app.screenSettings), + ) + if err != nil { + return nil, err } -) -func appStart(e *europi.EuroPi) { - if err := clock.Init(module.Config{ + return app, nil +} + +func (app *application) Start(e *europi.EuroPi) { + if err := app.clock.Init(module.Config{ BPM: 120.0, GateDuration: time.Millisecond * 100, Enabled: true, @@ -38,16 +60,12 @@ func appStart(e *europi.EuroPi) { } } -func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { - clock.Tick(deltaTime) +func (app *application) MainLoop(e *europi.EuroPi, deltaTime time.Duration) { + app.clock.Tick(deltaTime) } func main() { - var err error - ui, err = screenbank.NewScreenBank( - screenbank.WithScreen("main", "\u2b50", &screenMain), - screenbank.WithScreen("settings", "\u2611", &screenSettings), - ) + app, err := newApplication() if err != nil { panic(err) } @@ -57,11 +75,14 @@ func main() { if err := europi.Bootstrap( europi.EnableDisplayLogger(false), europi.InitRandom(true), - europi.AppStart(appStart), - europi.AppMainLoop(mainLoop), - europi.AppMainLoopInterval(time.Millisecond*1), - europi.UI(ui), - europi.UIRefreshRate(time.Millisecond*50), + europi.App( + app, + europi.AppMainLoopInterval(time.Millisecond*1), + ), + europi.UI( + app.ui, + europi.UIRefreshRate(time.Millisecond*50), + ), ); err != nil { panic(err) } diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index 90d1c0a..ae77b58 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -227,7 +227,7 @@ func (c *Clockwerk) updateDisplay() { var app Clockwerk -func startLoop(e *europi.EuroPi) { +func appStart(e *europi.EuroPi) { app.EuroPi = e app.clocks = DefaultFactor app.displayShouldUpdate = true @@ -288,7 +288,7 @@ func mainLoop() { } func main() { - startLoop(europi.New()) + appStart(europi.New()) // Check for clock updates every 2 seconds. ticker := time.NewTicker(ResetDelay) diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index facc6ce..d1f7f44 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -23,7 +23,7 @@ type MyApp struct { var myApp MyApp -func startLoop(e *europi.EuroPi) { +func appStart(e *europi.EuroPi) { myApp.staticCv = 5 // Demonstrate adding a IRQ handler to B1 and B2. @@ -89,7 +89,7 @@ func mainLoop(e *europi.EuroPi) { func main() { e := europi.New() - startLoop(e) + appStart(e) for { mainLoop(e) time.Sleep(time.Millisecond) diff --git a/internal/projects/randomskips/randomskips.go b/internal/projects/randomskips/randomskips.go index b4a0a97..fd50daf 100644 --- a/internal/projects/randomskips/randomskips.go +++ b/internal/projects/randomskips/randomskips.go @@ -12,23 +12,6 @@ import ( "github.com/awonak/EuroPiGo/internal/projects/randomskips/screen" ) -var ( - skip module.RandomSkips - clock clockgenerator.ClockGenerator - - ui *screenbank.ScreenBank - screenMain = screen.Main{ - RandomSkips: &skip, - Clock: &clock, - } - screenClock = clockScreen.Settings{ - Clock: &clock, - } - screenSettings = screen.Settings{ - RandomSkips: &skip, - } -) - func makeGate(out hal.VoltageOutput) func(value bool) { return func(value bool) { if value { @@ -39,39 +22,76 @@ func makeGate(out hal.VoltageOutput) func(value bool) { } } -func appStart(e *europi.EuroPi) { - if err := skip.Init(module.Config{ +type application struct { + skip *module.RandomSkips + clock *clockgenerator.ClockGenerator + + ui *screenbank.ScreenBank + screenMain screen.Main + screenClock clockScreen.Settings + screenSettings screen.Settings +} + +func newApplication() (*application, error) { + skip := &module.RandomSkips{} + clock := &clockgenerator.ClockGenerator{} + + app := &application{ + skip: skip, + clock: clock, + screenMain: screen.Main{ + RandomSkips: skip, + Clock: clock, + }, + screenClock: clockScreen.Settings{ + Clock: clock, + }, + screenSettings: screen.Settings{ + RandomSkips: skip, + }, + } + + var err error + app.ui, err = screenbank.NewScreenBank( + screenbank.WithScreen("main", "\u2b50", &app.screenMain), + screenbank.WithScreen("settings", "\u2611", &app.screenSettings), + screenbank.WithScreen("clock", "\u23f0", &app.screenClock), + ) + if err != nil { + return nil, err + } + + return app, nil +} + +func (app *application) Start(e *europi.EuroPi) { + if err := app.skip.Init(module.Config{ Gate: makeGate(e.CV1), Chance: 0.5, }); err != nil { panic(err) } - if err := clock.Init(clockgenerator.Config{ + if err := app.clock.Init(clockgenerator.Config{ BPM: 120.0, Enabled: false, - ClockOut: skip.Gate, + ClockOut: app.skip.Gate, }); err != nil { panic(err) } e.DI.HandlerEx(hal.ChangeAny, func(value bool, _ time.Duration) { - skip.Gate(value) + app.skip.Gate(value) }) } -func mainLoop(e *europi.EuroPi, deltaTime time.Duration) { - clock.Tick(deltaTime) - skip.Tick(deltaTime) +func (app *application) MainLoop(e *europi.EuroPi, deltaTime time.Duration) { + app.clock.Tick(deltaTime) + app.skip.Tick(deltaTime) } func main() { - var err error - ui, err = screenbank.NewScreenBank( - screenbank.WithScreen("main", "\u2b50", &screenMain), - screenbank.WithScreen("settings", "\u2611", &screenSettings), - screenbank.WithScreen("clock", "\u23f0", &screenClock), - ) + app, err := newApplication() if err != nil { panic(err) } @@ -81,11 +101,14 @@ func main() { if err := europi.Bootstrap( europi.EnableDisplayLogger(false), europi.InitRandom(true), - europi.AppStart(appStart), - europi.AppMainLoop(mainLoop), - europi.AppMainLoopInterval(time.Millisecond*1), - europi.UI(ui), - europi.UIRefreshRate(time.Millisecond*50), + europi.App( + app, + europi.AppMainLoopInterval(time.Millisecond*1), + ), + europi.UI( + app.ui, + europi.UIRefreshRate(time.Millisecond*50), + ), ); err != nil { panic(err) } From 7ff7672b865221b6284bb100c4e3764666c5aa78 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 29 Apr 2023 17:17:35 -0700 Subject: [PATCH 45/62] remapper support --- lerp/remap.go | 18 ++++++ lerp/remap32.go | 52 +++++++++++++++++ lerp/remap32_test.go | 124 ++++++++++++++++++++++++++++++++++++++++ lerp/remap64.go | 52 +++++++++++++++++ lerp/remap64_test.go | 124 ++++++++++++++++++++++++++++++++++++++++ lerp/remappoint.go | 48 ++++++++++++++++ lerp/remappoint_test.go | 100 ++++++++++++++++++++++++++++++++ 7 files changed, 518 insertions(+) create mode 100644 lerp/remap.go create mode 100644 lerp/remap32.go create mode 100644 lerp/remap32_test.go create mode 100644 lerp/remap64.go create mode 100644 lerp/remap64_test.go create mode 100644 lerp/remappoint.go create mode 100644 lerp/remappoint_test.go diff --git a/lerp/remap.go b/lerp/remap.go new file mode 100644 index 0000000..f79202e --- /dev/null +++ b/lerp/remap.go @@ -0,0 +1,18 @@ +package lerp + +type Remapable interface { + Lerpable +} + +type Remapper[TIn, TOut Remapable, F Float] interface { + Remap(value TIn) TOut + MCoeff() F + InputMinimum() TIn + InputMaximum() TIn + OutputMinimum() TOut + OutputMaximum() TOut +} + +type Remapper32[TIn, TOut Remapable] Remapper[TIn, TOut, float32] + +type Remapper64[TIn, TOut Remapable] Remapper[TIn, TOut, float64] diff --git a/lerp/remap32.go b/lerp/remap32.go new file mode 100644 index 0000000..4134ed8 --- /dev/null +++ b/lerp/remap32.go @@ -0,0 +1,52 @@ +package lerp + +type remap32[TIn, TOut Remapable] struct { + inMin TIn + inMax TIn + outMin TOut + r float32 +} + +func NewRemap32[TIn, TOut Remapable](inMin, inMax TIn, outMin, outMax TOut) Remapper32[TIn, TOut] { + var r float32 + // if rIn is 0, then we don't need to test further, we're always min (max) value + if rIn := inMax - inMin; rIn != 0 { + if rOut := outMax - outMin; rOut != 0 { + r = float32(rOut) / float32(rIn) + } + } + return remap32[TIn, TOut]{ + inMin: inMin, + inMax: inMax, + outMin: outMin, + r: r, + } +} + +func (r remap32[TIn, TOut]) Remap(value TIn) TOut { + if r.r == 0.0 { + return r.outMin + } + + return r.outMin + TOut(r.r*float32(value-r.inMin)) +} + +func (r remap32[TIn, TOut]) MCoeff() float32 { + return r.r +} + +func (r remap32[TIn, TOut]) InputMinimum() TIn { + return r.inMin +} + +func (r remap32[TIn, TOut]) InputMaximum() TIn { + return r.inMax +} + +func (r remap32[TIn, TOut]) OutputMinimum() TOut { + return r.outMin +} + +func (r remap32[TIn, TOut]) OutputMaximum() TOut { + return r.Remap(r.inMax) +} diff --git a/lerp/remap32_test.go b/lerp/remap32_test.go new file mode 100644 index 0000000..e0b50d4 --- /dev/null +++ b/lerp/remap32_test.go @@ -0,0 +1,124 @@ +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) + if expected, actual := float32(4.39822971502571), 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("MCoeff", 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 := float32(0.0), l.MCoeff(); actual != expected { + t.Fatalf("Remap32[%v, %v, %v, %v] MCoeff: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual) + } + }) + t.Run("NonZeroRange", 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(outMax-outMin)/float32(inMax-inMin), l.MCoeff(); actual != expected { + t.Fatalf("Remap32[%v, %v, %v, %v] MCoeff: 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..e25854f --- /dev/null +++ b/lerp/remap64.go @@ -0,0 +1,52 @@ +package lerp + +type remap64[TIn, TOut Remapable] struct { + inMin TIn + inMax TIn + outMin TOut + r float64 +} + +func NewRemap64[TIn, TOut Remapable](inMin, inMax TIn, outMin, outMax TOut) Remapper64[TIn, TOut] { + var r float64 + // if rIn is 0, then we don't need to test further, we're always min (max) value + if rIn := inMax - inMin; rIn != 0 { + if rOut := outMax - outMin; rOut != 0 { + r = float64(rOut) / float64(rIn) + } + } + return remap64[TIn, TOut]{ + inMin: inMin, + inMax: inMax, + outMin: outMin, + r: r, + } +} + +func (r remap64[TIn, TOut]) Remap(value TIn) TOut { + if r.r == 0.0 { + return r.outMin + } + + return r.outMin + TOut(r.r*float64(value-r.inMin)) +} + +func (r remap64[TIn, TOut]) MCoeff() float64 { + return r.r +} + +func (r remap64[TIn, TOut]) InputMinimum() TIn { + return r.inMin +} + +func (r remap64[TIn, TOut]) InputMaximum() TIn { + return r.inMax +} + +func (r remap64[TIn, TOut]) OutputMinimum() TOut { + return r.outMin +} + +func (r remap64[TIn, TOut]) OutputMaximum() TOut { + return r.Remap(r.inMax) +} diff --git a/lerp/remap64_test.go b/lerp/remap64_test.go new file mode 100644 index 0000000..369836e --- /dev/null +++ b/lerp/remap64_test.go @@ -0,0 +1,124 @@ +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("MCoeff", 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 := float64(0.0), l.MCoeff(); actual != expected { + t.Fatalf("Remap64[%v, %v, %v, %v] MCoeff: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual) + } + }) + t.Run("NonZeroRange", 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(outMax-outMin)/float64(inMax-inMin), l.MCoeff(); actual != expected { + t.Fatalf("Remap64[%v, %v, %v, %v] MCoeff: 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/lerp/remappoint.go b/lerp/remappoint.go new file mode 100644 index 0000000..28152dd --- /dev/null +++ b/lerp/remappoint.go @@ -0,0 +1,48 @@ +package lerp + +// This is the world's worst Lerp remapper. Regardless of the input value, it always returns the output value. + +type remapPoint[TIn, TOut Remapable, TFloat Float] struct { + in TIn + out TOut +} + +func NewRemapPoint[TIn, TOut Remapable, TFloat Float](in TIn, out TOut) Remapper[TIn, TOut, TFloat] { + return remapPoint[TIn, TOut, TFloat]{ + in: in, + out: out, + } +} + +func NewRemapPoint32[TIn, TOut Remapable](in TIn, out TOut) Remapper[TIn, TOut, float32] { + return NewRemapPoint[TIn, TOut, float32](in, out) +} + +func NewRemapPoint64[TIn, TOut Remapable](in TIn, out TOut) Remapper[TIn, TOut, float64] { + return NewRemapPoint[TIn, TOut, float64](in, out) +} + +func (r remapPoint[TIn, TOut, TFloat]) Remap(value TIn) TOut { + // `value` isn't used here - just return `out` + return r.out +} + +func (r remapPoint[TIn, TOut, TFloat]) MCoeff() TFloat { + return 0.0 +} + +func (r remapPoint[TIn, TOut, TFloat]) InputMinimum() TIn { + return r.in +} + +func (r remapPoint[TIn, TOut, TFloat]) InputMaximum() TIn { + return r.in +} + +func (r remapPoint[TIn, TOut, TFloat]) OutputMinimum() TOut { + return r.out +} + +func (r remapPoint[TIn, TOut, TFloat]) OutputMaximum() TOut { + return r.out +} diff --git a/lerp/remappoint_test.go b/lerp/remappoint_test.go new file mode 100644 index 0000000..a974057 --- /dev/null +++ b/lerp/remappoint_test.go @@ -0,0 +1,100 @@ +package lerp_test + +import ( + "math" + "testing" + + "github.com/awonak/EuroPiGo/lerp" +) + +func TestRemapPoint(t *testing.T) { + t.Run("New", func(t *testing.T) { + t.Run("NewRemapPoint32", func(t *testing.T) { + in, out := 0, float32(math.Pi) + if actual := lerp.NewRemapPoint32(in, out); actual == nil { + t.Fatalf("RemapPoint[%v, %v] NewRemapPoint32: expected[non-nil] actual[nil]", in, out) + } + }) + t.Run("NewRemapPoint64", func(t *testing.T) { + in, out := 0, float32(math.Pi) + if actual := lerp.NewRemapPoint64(in, out); actual == nil { + t.Fatalf("RemapPoint[%v, %v] NewRemapPoint64: expected[non-nil] actual[nil]", in, out) + } + }) + }) + + t.Run("Remap", func(t *testing.T) { + t.Run("ZeroRange", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := out, l.Remap(in); actual != expected { + t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + t.Run("InRange", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := out, l.Remap(in); actual != expected { + t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + + t.Run("OutOfRange", func(t *testing.T) { + t.Run("BelowMin", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := out, l.Remap(-2); actual != expected { + t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + + t.Run("AboveMax", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := out, l.Remap(12); actual != expected { + t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + }) + }) + + t.Run("MCoeff", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := float32(0.0), l.MCoeff(); actual != expected { + t.Fatalf("RemapPoint[%v, %v] MCoeff: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + + t.Run("InputMinimum", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := in, l.InputMinimum(); actual != expected { + t.Fatalf("RemapPoint[%v, %v] InputMinimum: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + + t.Run("InputMaximum", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := in, l.InputMaximum(); actual != expected { + t.Fatalf("RemapPoint[%v, %v] InputMaximum: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + + t.Run("OutputMinimum", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := out, l.OutputMinimum(); actual != expected { + t.Fatalf("RemapPoint[%v, %v] OutputMinimum: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + + t.Run("OutputMaximum", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := out, l.OutputMaximum(); actual != expected { + t.Fatalf("RemapPoint[%v, %v] OutputMaximum: expected[%v] actual[%v]", in, out, expected, actual) + } + }) +} From 37f0c04165d1c72eb86133e3b247b1a8c18e9963 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 29 Apr 2023 21:06:29 -0700 Subject: [PATCH 46/62] allow for much better calibration of voltage - add support for bipolar CV setting and getting - clean up knob bank a little --- experimental/envelope/map.go | 11 +++++ experimental/envelope/map32.go | 73 +++++++++++++++++++++++++++++++ experimental/envelope/map64.go | 73 +++++++++++++++++++++++++++++++ experimental/envelope/mapentry.go | 22 ++++++++++ experimental/knobbank/knobbank.go | 32 +++----------- hardware/hal/analoginput.go | 12 +++-- hardware/hal/voltageoutput.go | 7 +-- hardware/rev1/analoginput.go | 52 +++++++++++++++------- hardware/rev1/platform.go | 46 +++++++------------ hardware/rev1/voltageoutput.go | 35 ++++++++++----- internal/nonpico/rev1/pwm.go | 27 +++++++++--- internal/pico/adc.go | 4 +- internal/pico/digitalreader.go | 3 +- internal/pico/pwm.go | 32 +++++++++++--- 14 files changed, 323 insertions(+), 106 deletions(-) create mode 100644 experimental/envelope/map.go create mode 100644 experimental/envelope/map32.go create mode 100644 experimental/envelope/map64.go create mode 100644 experimental/envelope/mapentry.go diff --git a/experimental/envelope/map.go b/experimental/envelope/map.go new file mode 100644 index 0000000..9c1249a --- /dev/null +++ b/experimental/envelope/map.go @@ -0,0 +1,11 @@ +package envelope + +import "github.com/awonak/EuroPiGo/lerp" + +type Map[TIn, TOut lerp.Lerpable] interface { + Remap(value TIn) TOut + InputMinimum() TIn + InputMaximum() TIn + OutputMinimum() TOut + OutputMaximum() TOut +} diff --git a/experimental/envelope/map32.go b/experimental/envelope/map32.go new file mode 100644 index 0000000..aee3524 --- /dev/null +++ b/experimental/envelope/map32.go @@ -0,0 +1,73 @@ +package envelope + +import ( + "sort" + + "github.com/awonak/EuroPiGo/lerp" +) + +type envMap32[TIn, TOut lerp.Lerpable] struct { + rem []lerp.Remapper32[TIn, TOut] + outMax TOut +} + +func NewMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { + if len(points) == 0 { + panic("must have at least 1 point") + } + + p := make(MapEntryList[TIn, TOut], len(points)) + // make a copy just in case we're dealing with another goroutine's data + copy(p, points) + // ensure it's sorted + sort.Sort(p) + + var outMax TOut + var rem []lerp.Remapper32[TIn, TOut] + if len(p) == 1 { + cur := p[0] + outMax = cur.Output + rem = append(rem, lerp.NewRemapPoint[TIn, TOut, float32](cur.Input, cur.Output)) + } else { + for pos := 0; pos < len(p)-1; pos++ { + cur, next := p[pos], p[pos+1] + outMax = next.Output + rem = append(rem, lerp.NewRemap32(cur.Input, next.Input, cur.Output, next.Output)) + } + } + return &envMap32[TIn, TOut]{ + rem: rem, + outMax: outMax, + } +} + +func (m *envMap32[TIn, TOut]) Remap(value TIn) TOut { + for _, r := range m.rem { + if value < r.InputMinimum() { + return r.OutputMinimum() + } else if value < r.InputMaximum() { + return r.Remap(value) + } + } + + return m.outMax +} + +func (m *envMap32[TIn, TOut]) InputMinimum() TIn { + // we're guaranteed to have 1 point + return m.rem[0].InputMaximum() +} + +func (m *envMap32[TIn, TOut]) InputMaximum() TIn { + // we're guaranteed to have 1 point + return m.rem[len(m.rem)-1].InputMaximum() +} + +func (m *envMap32[TIn, TOut]) OutputMinimum() TOut { + // we're guaranteed to have 1 point + return m.rem[0].OutputMinimum() +} + +func (m *envMap32[TIn, TOut]) OutputMaximum() TOut { + return m.outMax +} diff --git a/experimental/envelope/map64.go b/experimental/envelope/map64.go new file mode 100644 index 0000000..0e687b2 --- /dev/null +++ b/experimental/envelope/map64.go @@ -0,0 +1,73 @@ +package envelope + +import ( + "sort" + + "github.com/awonak/EuroPiGo/lerp" +) + +type envMap64[TIn, TOut lerp.Lerpable] struct { + rem []lerp.Remapper64[TIn, TOut] + outMax TOut +} + +func NewMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { + if len(points) == 0 { + panic("must have at least 1 point") + } + + p := make(MapEntryList[TIn, TOut], len(points)) + // make a copy just in case we're dealing with another goroutine's data + copy(p, points) + // ensure it's sorted + sort.Sort(p) + + var outMax TOut + var rem []lerp.Remapper64[TIn, TOut] + if len(p) == 1 { + cur := p[0] + outMax = cur.Output + rem = append(rem, lerp.NewRemapPoint[TIn, TOut, float64](cur.Input, cur.Output)) + } else { + for pos := 0; pos < len(p)-1; pos++ { + cur, next := p[pos], p[pos+1] + outMax = next.Output + rem = append(rem, lerp.NewRemap64(cur.Input, next.Input, cur.Output, next.Output)) + } + } + return &envMap64[TIn, TOut]{ + rem: rem, + outMax: outMax, + } +} + +func (m *envMap64[TIn, TOut]) Remap(value TIn) TOut { + for _, r := range m.rem { + if value < r.InputMinimum() { + return r.OutputMinimum() + } else if value < r.InputMaximum() { + return r.Remap(value) + } + } + + return m.outMax +} + +func (m *envMap64[TIn, TOut]) InputMinimum() TIn { + // we're guaranteed to have 1 point + return m.rem[0].InputMaximum() +} + +func (m *envMap64[TIn, TOut]) InputMaximum() TIn { + // we're guaranteed to have 1 point + return m.rem[len(m.rem)-1].InputMaximum() +} + +func (m *envMap64[TIn, TOut]) OutputMinimum() TOut { + // we're guaranteed to have 1 point + return m.rem[0].OutputMinimum() +} + +func (m *envMap64[TIn, TOut]) OutputMaximum() TOut { + return m.outMax +} diff --git a/experimental/envelope/mapentry.go b/experimental/envelope/mapentry.go new file mode 100644 index 0000000..6c74f88 --- /dev/null +++ b/experimental/envelope/mapentry.go @@ -0,0 +1,22 @@ +package envelope + +import "github.com/awonak/EuroPiGo/lerp" + +type MapEntry[TIn, TOut lerp.Lerpable] struct { + Input TIn + Output TOut +} + +type MapEntryList[TIn, TOut lerp.Lerpable] []MapEntry[TIn, TOut] + +func (m MapEntryList[TIn, TOut]) Len() int { + return len(m) +} + +func (m MapEntryList[TIn, TOut]) Less(i, j int) bool { + return m[i].Input < m[j].Input +} + +func (m MapEntryList[TIn, TOut]) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} diff --git a/experimental/knobbank/knobbank.go b/experimental/knobbank/knobbank.go index 6854f67..9f7a062 100644 --- a/experimental/knobbank/knobbank.go +++ b/experimental/knobbank/knobbank.go @@ -3,13 +3,11 @@ package knobbank import ( "errors" - "github.com/awonak/EuroPiGo/clamp" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/units" ) type KnobBank struct { - knob hal.KnobInput + hal.KnobInput current int lastValue float32 bank []knobBankEntry @@ -21,7 +19,7 @@ func NewKnobBank(knob hal.KnobInput, opts ...KnobBankOption) (*KnobBank, error) } kb := &KnobBank{ - knob: knob, + KnobInput: knob, lastValue: knob.ReadVoltage(), } @@ -55,42 +53,26 @@ func (kb *KnobBank) Current() hal.KnobInput { return kb } -func (kb *KnobBank) MinVoltage() float32 { - return kb.knob.MinVoltage() -} - -func (kb *KnobBank) MaxVoltage() float32 { - return kb.knob.MaxVoltage() -} - func (kb *KnobBank) ReadVoltage() float32 { - value := kb.knob.ReadVoltage() + value := kb.KnobInput.ReadVoltage() if len(kb.bank) == 0 { return value } cur := &kb.bank[kb.current] - percent := kb.knob.Percent() + percent := kb.Percent() kb.lastValue = cur.update(percent, value, kb.lastValue) return cur.Value() } -func (kb *KnobBank) ReadCV() units.CV { - return units.CV(clamp.Clamp(kb.Percent(), 0.0, 1.0)) -} - -func (kb *KnobBank) ReadVOct() units.VOct { - return units.VOct(kb.ReadVoltage()) -} - func (kb *KnobBank) Percent() float32 { - percent := kb.knob.Percent() + percent := kb.KnobInput.Percent() if len(kb.bank) == 0 { return percent } cur := &kb.bank[kb.current] - value := kb.knob.ReadVoltage() + value := kb.KnobInput.ReadVoltage() kb.lastValue = cur.update(percent, value, kb.lastValue) return cur.Percent() } @@ -102,7 +84,7 @@ func (kb *KnobBank) Next() { } cur := &kb.bank[kb.current] - cur.lock(kb.knob, kb.lastValue) + cur.lock(kb.KnobInput, kb.lastValue) kb.current++ if kb.current >= len(kb.bank) { diff --git a/hardware/hal/analoginput.go b/hardware/hal/analoginput.go index 12ae33b..010d01c 100644 --- a/hardware/hal/analoginput.go +++ b/hardware/hal/analoginput.go @@ -1,19 +1,23 @@ package hal -import "github.com/awonak/EuroPiGo/units" +import ( + "github.com/awonak/EuroPiGo/experimental/envelope" + "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 - CalibratedMinAI uint16 - CalibratedMaxAI uint16 + Samples int + Calibration envelope.Map[uint16, float32] } diff --git a/hardware/hal/voltageoutput.go b/hardware/hal/voltageoutput.go index 1407619..ac3f89e 100644 --- a/hardware/hal/voltageoutput.go +++ b/hardware/hal/voltageoutput.go @@ -3,12 +3,14 @@ package hal import ( "time" + "github.com/awonak/EuroPiGo/experimental/envelope" "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 @@ -16,7 +18,6 @@ type VoltageOutput interface { } type VoltageOutputConfig struct { - Period time.Duration - Offset uint16 - Top uint16 + Period time.Duration + Calibration envelope.Map[float32, uint16] } diff --git a/hardware/rev1/analoginput.go b/hardware/rev1/analoginput.go index 0ac3347..aeba5da 100644 --- a/hardware/rev1/analoginput.go +++ b/hardware/rev1/analoginput.go @@ -4,8 +4,8 @@ import ( "errors" "github.com/awonak/EuroPiGo/clamp" + "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/lerp" "github.com/awonak/EuroPiGo/units" ) @@ -26,7 +26,7 @@ const ( type analoginput struct { adc ADCProvider samples int - cal lerp.Lerper32[uint16] + cal envelope.Map[uint16, float32] } var ( @@ -45,7 +45,16 @@ func newAnalogInput(adc ADCProvider) *analoginput { return &analoginput{ adc: adc, samples: DefaultSamples, - cal: lerp.NewLerp32[uint16](DefaultCalibratedMinAI, DefaultCalibratedMaxAI), + cal: envelope.NewMap32([]envelope.MapEntry[uint16, float32]{ + { + Input: DefaultCalibratedMinAI, + Output: MinInputVoltage, + }, + { + Input: DefaultCalibratedMaxAI, + Output: MaxInputVoltage, + }, + }), } } @@ -55,38 +64,47 @@ func (a *analoginput) Configure(config hal.AnalogInputConfig) error { return errors.New("samples must be non-zero") } - if config.CalibratedMinAI != 0 || config.CalibratedMaxAI != 0 { - if config.CalibratedMinAI == config.CalibratedMaxAI { - return errors.New("calibratedminai and calibratedmaxai must be different") - } else if config.CalibratedMinAI > config.CalibratedMaxAI { - return errors.New("calibtatedminai must be less than calibratedmaxai") - } - a.cal = lerp.NewLerp32(config.CalibratedMinAI, config.CalibratedMaxAI) + 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.cal.InverseLerp(a.adc.Get(a.samples)) + return a.ReadVoltage() / MaxInputVoltage } // ReadVoltage returns the current read voltage between 0.0 and 10.0 volts. func (a *analoginput) ReadVoltage() float32 { - // NOTE: if MinInputVoltage ever becomes non-zero, then we need to use a lerp instead - return a.Percent() * MaxInputVoltage + rawVoltage := a.ReadRawVoltage() + return a.cal.Remap(rawVoltage) } // ReadCV returns the current read voltage as a CV value. func (a *analoginput) ReadCV() units.CV { - // we can't use a.Percent() here, because we might get over 5.0 volts input - // just clamp it 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()) @@ -94,10 +112,10 @@ func (a *analoginput) ReadVOct() units.VOct { // MinVoltage returns the minimum voltage that that input can ever read by this device func (a *analoginput) MinVoltage() float32 { - return MinInputVoltage + return a.cal.OutputMinimum() } // MaxVoltage returns the maximum voltage that the input can ever read by this device func (a *analoginput) MaxVoltage() float32 { - return MaxInputVoltage + return a.cal.OutputMaximum() } diff --git a/hardware/rev1/platform.go b/hardware/rev1/platform.go index b3f3734..d0709cc 100644 --- a/hardware/rev1/platform.go +++ b/hardware/rev1/platform.go @@ -26,53 +26,39 @@ var ( // 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 switch hw { case hal.HardwareIdDigital1Input: - t, _ := InputDigital1.(T) - return t + t, _ = InputDigital1.(T) case hal.HardwareIdAnalog1Input: - t, _ := InputAnalog1.(T) - return t + t, _ = InputAnalog1.(T) case hal.HardwareIdDisplay1Output: - t, _ := OutputDisplay1.(T) - return t + t, _ = OutputDisplay1.(T) case hal.HardwareIdButton1Input: - t, _ := InputButton1.(T) - return t + t, _ = InputButton1.(T) case hal.HardwareIdButton2Input: - t, _ := InputButton2.(T) - return t + t, _ = InputButton2.(T) case hal.HardwareIdKnob1Input: - t, _ := InputKnob1.(T) - return t + t, _ = InputKnob1.(T) case hal.HardwareIdKnob2Input: - t, _ := InputKnob2.(T) - return t + t, _ = InputKnob2.(T) case hal.HardwareIdVoltage1Output: - t, _ := OutputVoltage1.(T) - return t + t, _ = OutputVoltage1.(T) case hal.HardwareIdVoltage2Output: - t, _ := OutputVoltage2.(T) - return t + t, _ = OutputVoltage2.(T) case hal.HardwareIdVoltage3Output: - t, _ := OutputVoltage3.(T) - return t + t, _ = OutputVoltage3.(T) case hal.HardwareIdVoltage4Output: - t, _ := OutputVoltage4.(T) - return t + t, _ = OutputVoltage4.(T) case hal.HardwareIdVoltage5Output: - t, _ := OutputVoltage5.(T) - return t + t, _ = OutputVoltage5.(T) case hal.HardwareIdVoltage6Output: - t, _ := OutputVoltage6.(T) - return t + t, _ = OutputVoltage6.(T) case hal.HardwareIdRandom1Generator: - t, _ := DeviceRandomGenerator1.(T) - return t + t, _ = DeviceRandomGenerator1.(T) default: - var none T - return none } + return t } // Initialize sets up the hardware diff --git a/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go index 5d9bf46..07758c7 100644 --- a/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/awonak/EuroPiGo/clamp" + "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/units" ) @@ -27,7 +27,6 @@ var defaultPeriod time.Duration = time.Nanosecond * 500 // voltageoutput is struct for interacting with the CV/VOct voltage output jacks. type voltageoutput struct { pwm PWMProvider - ofs uint16 } var ( @@ -39,8 +38,10 @@ var ( type PWMProvider interface { Configure(config hal.VoltageOutputConfig) error - Set(v float32, ofs uint16) + Set(v float32) Get() float32 + MinVoltage() float32 + MaxVoltage() float32 } // NewOutput returns a new Output interface. @@ -50,8 +51,16 @@ func newVoltageOuput(pwm PWMProvider) hal.VoltageOutput { } err := o.Configure(hal.VoltageOutputConfig{ Period: defaultPeriod, - Offset: CalibratedOffset, - Top: CalibratedTop, + Calibration: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ + { + Input: MinOutputVoltage, + Output: CalibratedTop, + }, + { + Input: MaxOutputVoltage, + Output: CalibratedOffset, + }, + }), }) if err != nil { panic(fmt.Errorf("configuration error: %v", err.Error())) @@ -66,15 +75,12 @@ func (o *voltageoutput) Configure(config hal.VoltageOutputConfig) error { return err } - o.ofs = config.Offset - return nil } // SetVoltage sets the current output voltage within a range of 0.0 to 10.0. func (o *voltageoutput) SetVoltage(v float32) { - v = clamp.Clamp(v, MinOutputVoltage, MaxOutputVoltage) - o.pwm.Set(v/MaxOutputVoltage, o.ofs) + o.pwm.Set(v) } // SetCV sets the current output voltage based on a CV value @@ -82,6 +88,11 @@ 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()) @@ -89,15 +100,15 @@ func (o *voltageoutput) SetVOct(voct units.VOct) { // Voltage returns the current voltage func (o *voltageoutput) Voltage() float32 { - return o.pwm.Get() * MaxOutputVoltage + return o.pwm.Get() } // MinVoltage returns the minimum voltage this device will output func (o *voltageoutput) MinVoltage() float32 { - return MinOutputVoltage + return o.pwm.MinVoltage() } // MaxVoltage returns the maximum voltage this device will output func (o *voltageoutput) MaxVoltage() float32 { - return MaxOutputVoltage + return o.pwm.MaxVoltage() } diff --git a/internal/nonpico/rev1/pwm.go b/internal/nonpico/rev1/pwm.go index 1a366c8..766d4ab 100644 --- a/internal/nonpico/rev1/pwm.go +++ b/internal/nonpico/rev1/pwm.go @@ -5,9 +5,9 @@ package rev1 import ( "fmt" - "math" "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev1" ) @@ -15,6 +15,7 @@ import ( type nonPicoPwm struct { bus event.Bus id hal.HardwareId + cal envelope.Map[float32, uint16] v float32 } @@ -22,6 +23,16 @@ func newNonPicoPwm(bus event.Bus, id hal.HardwareId) rev1.PWMProvider { p := &nonPicoPwm{ bus: bus, id: id, + cal: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ + { + Input: rev1.MinOutputVoltage, + Output: rev1.CalibratedTop, + }, + { + Input: rev1.MaxOutputVoltage, + Output: rev1.CalibratedOffset, + }, + }), } return p } @@ -30,10 +41,8 @@ func (p *nonPicoPwm) Configure(config hal.VoltageOutputConfig) error { return nil } -func (p *nonPicoPwm) Set(v float32, ofs uint16) { - invertedV := v * math.MaxUint16 - // volts := (float32(o.pwm.Top()) - invertedCv) - o.ofs - volts := invertedV - float32(ofs) +func (p *nonPicoPwm) Set(v float32) { + volts := p.cal.Remap(v) p.v = v p.bus.Post(fmt.Sprintf("hw_pwm_%d", p.id), HwMessagePwmValue{ Value: uint16(volts), @@ -43,3 +52,11 @@ func (p *nonPicoPwm) Set(v float32, ofs uint16) { 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/pico/adc.go b/internal/pico/adc.go index 80fdc4a..2ad5a17 100644 --- a/internal/pico/adc.go +++ b/internal/pico/adc.go @@ -7,8 +7,6 @@ import ( "machine" "runtime/interrupt" "sync" - - "github.com/awonak/EuroPiGo/hardware/rev1" ) var ( @@ -19,7 +17,7 @@ type picoAdc struct { adc machine.ADC } -func newPicoAdc(pin machine.Pin) rev1.ADCProvider { +func newPicoAdc(pin machine.Pin) *picoAdc { adcOnce.Do(machine.InitADC) adc := &picoAdc{ diff --git a/internal/pico/digitalreader.go b/internal/pico/digitalreader.go index bf06ca1..db4442e 100644 --- a/internal/pico/digitalreader.go +++ b/internal/pico/digitalreader.go @@ -8,14 +8,13 @@ import ( "runtime/interrupt" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" ) type picoDigitalReader struct { pin machine.Pin } -func newPicoDigitalReader(pin machine.Pin) rev1.DigitalReaderProvider { +func newPicoDigitalReader(pin machine.Pin) *picoDigitalReader { dr := &picoDigitalReader{ pin: pin, } diff --git a/internal/pico/pwm.go b/internal/pico/pwm.go index f00cd2a..3db55b2 100644 --- a/internal/pico/pwm.go +++ b/internal/pico/pwm.go @@ -10,6 +10,7 @@ import ( "runtime/interrupt" "runtime/volatile" + "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev1" ) @@ -19,6 +20,7 @@ type picoPwm struct { pin machine.Pin ch uint8 v uint32 + cal envelope.Map[float32, uint16] } // pwmGroup is an interface for interacting with a machine.pwmGroup @@ -36,6 +38,16 @@ func newPicoPwm(pwm pwmGroup, pin machine.Pin) rev1.PWMProvider { p := &picoPwm{ pwm: pwm, pin: pin, + cal: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ + { + Input: rev1.MinOutputVoltage, + Output: rev1.CalibratedTop, + }, + { + Input: rev1.MaxOutputVoltage, + Output: rev1.CalibratedOffset, + }, + }), } return p } @@ -51,7 +63,11 @@ func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error { return fmt.Errorf("pwm Configure error: %w", err) } - p.pwm.SetTop(uint32(config.Top)) + if config.Calibration != nil { + p.cal = config.Calibration + } + + 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) @@ -61,10 +77,8 @@ func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error { return nil } -func (p *picoPwm) Set(v float32, ofs uint16) { - invertedV := v * float32(p.pwm.Top()) - // volts := (float32(o.pwm.Top()) - invertedCv) - o.ofs - volts := invertedV - float32(ofs) +func (p *picoPwm) Set(v float32) { + volts := p.cal.Remap(v) state := interrupt.Disable() p.pwm.Set(p.ch, uint32(volts)) interrupt.Restore(state) @@ -74,3 +88,11 @@ func (p *picoPwm) Set(v float32, ofs uint16) { func (p *picoPwm) Get() float32 { return math.Float32frombits(volatile.LoadUint32(&p.v)) } + +func (p *picoPwm) MinVoltage() float32 { + return p.cal.InputMinimum() +} + +func (p *picoPwm) MaxVoltage() float32 { + return p.cal.InputMaximum() +} From 8273242efa9da33e8a60d88fd1cbe9ff10c9b734 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Wed, 3 May 2023 20:14:50 -0700 Subject: [PATCH 47/62] better support for rev0 - fix numerous usability issues with offline debugger - fixed envelope mapping on analog and pwm devices --- bootstrap.go | 33 +-- bootstrap_features.go | 36 +++- bootstrap_lifecycle.go | 22 +- bootstrap_nonpico.go | 12 +- bootstrap_nonpico_panic.go | 8 + bootstrap_panic.go | 21 +- ...bled.go => bootstrap_pico_panicdisabled.go | 4 +- ...abled.go => bootstrap_pico_panicenabled.go | 4 +- bootstrap_ui.go | 45 ++-- bootstrap_uimodule.go | 57 ++--- bootstrapoptions.go | 4 +- bootstrapoptions_app.go | 30 ++- bootstrapoptions_app_conversion.go | 111 ++++++++++ bootstrapoptions_features.go | 2 +- bootstrapoptions_lifecycle.go | 18 +- bootstrapoptions_ui.go | 4 +- europi.go | 117 ++++++---- europi_test.go | 34 ++- experimental/envelope/map.go | 6 + experimental/envelope/map32.go | 64 ++++-- experimental/envelope/map64.go | 64 ++++-- experimental/knobmenu/knobmenu.go | 9 +- experimental/screenbank/screenbank.go | 32 +-- experimental/screenbank/screenbankentry.go | 11 +- experimental/screenbank/screenbankoptions.go | 118 +++++++++- go.mod | 3 +- hardware/README.md | 38 ++-- hardware/common/analoginput.go | 103 +++++++++ hardware/{rev1 => common}/digitalinput.go | 29 +-- hardware/{rev1 => common}/displayoutput.go | 29 +-- hardware/{rev1 => common}/randomgenerator.go | 14 +- hardware/common/voltageoutput.go | 85 ++++++++ hardware/hal/hardware.go | 16 +- hardware/hal/voltageoutput.go | 5 +- hardware/platform.go | 3 + hardware/rev0/README.md | 21 ++ hardware/rev0/analoginput.go | 34 +++ hardware/rev0/hardware.go | 41 ++++ hardware/rev0/platform.go | 162 ++++++++++++++ hardware/rev0/voltageoutput.go | 40 ++++ hardware/rev1/README.md | 1 - hardware/rev1/analoginput.go | 95 +------- hardware/rev1/hardware.go | 41 ++++ hardware/rev1/platform.go | 203 +++++++++++------- hardware/rev1/voltageoutput.go | 84 +------- internal/nonpico/{rev1 => common}/adc.go | 11 +- .../nonpico/{rev1 => common}/digitalreader.go | 18 +- internal/nonpico/common/displaymode.go | 11 + .../nonpico/{rev1 => common}/displayoutput.go | 11 +- internal/nonpico/{rev1 => common}/messages.go | 5 +- internal/nonpico/{rev1 => common}/pwm.go | 29 ++- internal/nonpico/nonpico.go | 7 + internal/nonpico/rev0.go | 14 ++ internal/nonpico/rev0/api.go | 26 +++ internal/nonpico/rev0/listeners.go | 77 +++++++ internal/nonpico/rev0/platform.go | 35 +++ internal/nonpico/rev0/site/index.html | 173 +++++++++++++++ internal/nonpico/rev0/wsactivation.go | 126 +++++++++++ internal/nonpico/rev1/api.go | 9 +- internal/nonpico/rev1/displaymode.go | 11 - internal/nonpico/rev1/listeners.go | 24 +-- internal/nonpico/rev1/platform.go | 39 ++-- internal/nonpico/rev1/wsactivation.go | 19 +- internal/nonpico/wsactivator.go | 10 +- internal/pico/display.go | 3 +- internal/pico/pico.go | 34 ++- internal/pico/pwm.go | 51 +++-- .../projects/clockgenerator/screen/main.go | 4 +- internal/projects/clockwerk/clockwerk.go | 21 +- internal/projects/diagnostics/diagnostics.go | 14 +- internal/projects/randomskips/screen/main.go | 4 +- lerp/remap.go | 1 + lerp/remap32.go | 26 ++- lerp/remap32_test.go | 54 +++++ lerp/remap64.go | 26 ++- lerp/remap64_test.go | 54 +++++ lerp/remappoint.go | 5 + lerp/remappoint_test.go | 29 +++ 78 files changed, 2223 insertions(+), 671 deletions(-) create mode 100644 bootstrap_nonpico_panic.go rename bootstrap_panicdisabled.go => bootstrap_pico_panicdisabled.go (85%) rename bootstrap_panicenabled.go => bootstrap_pico_panicenabled.go (56%) create mode 100644 bootstrapoptions_app_conversion.go create mode 100644 hardware/common/analoginput.go rename hardware/{rev1 => common}/digitalinput.go (69%) rename hardware/{rev1 => common}/displayoutput.go (58%) rename hardware/{rev1 => common}/randomgenerator.go (60%) create mode 100644 hardware/common/voltageoutput.go create mode 100644 hardware/rev0/README.md create mode 100644 hardware/rev0/analoginput.go create mode 100644 hardware/rev0/hardware.go create mode 100644 hardware/rev0/platform.go create mode 100644 hardware/rev0/voltageoutput.go create mode 100644 hardware/rev1/hardware.go rename internal/nonpico/{rev1 => common}/adc.go (73%) rename internal/nonpico/{rev1 => common}/digitalreader.go (66%) create mode 100644 internal/nonpico/common/displaymode.go rename internal/nonpico/{rev1 => common}/displayoutput.go (82%) rename internal/nonpico/{rev1 => common}/messages.go (94%) rename internal/nonpico/{rev1 => common}/pwm.go (64%) create mode 100644 internal/nonpico/rev0.go create mode 100644 internal/nonpico/rev0/api.go create mode 100644 internal/nonpico/rev0/listeners.go create mode 100644 internal/nonpico/rev0/platform.go create mode 100644 internal/nonpico/rev0/site/index.html create mode 100644 internal/nonpico/rev0/wsactivation.go delete mode 100644 internal/nonpico/rev1/displaymode.go diff --git a/bootstrap.go b/bootstrap.go index 5aafe52..e2f17f6 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -1,6 +1,7 @@ package europi import ( + "context" "errors" "sync" "time" @@ -8,7 +9,7 @@ import ( var ( // Pi is a global EuroPi instance constructed by calling the Bootstrap() function - Pi *EuroPi + Pi Hardware piWantDestroyChan chan any ) @@ -77,14 +78,16 @@ func Bootstrap(options ...BootstrapOption) error { var ( onceBootstrapDestroy sync.Once - nonPicoWSApi nonPicoWSActivation + nonPicoWSApi NonPicoWSActivation ) panicHandler := config.panicHandler lastDestroyFunc := config.onBeginDestroyFn + ctx, cancel := context.WithCancel(context.TODO()) runBootstrapDestroy := func() { reason := recover() + cancel() if reason != nil && panicHandler != nil { - config.onBeginDestroyFn = func(e *EuroPi, reason any) { + config.onBeginDestroyFn = func(e Hardware, reason any) { if lastDestroyFunc != nil { lastDestroyFunc(e, reason) } @@ -101,7 +104,7 @@ func Bootstrap(options ...BootstrapOption) error { config.onPostBootstrapConstructionFn(e) } - nonPicoWSApi = bootstrapInitializeComponents(&config, e) + nonPicoWSApi = bootstrapInitializeComponents(ctx, &config, e) if config.onBootstrapCompletedFn != nil { config.onBootstrapCompletedFn(e) @@ -121,7 +124,7 @@ func Shutdown(reason any) error { return nil } -func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) nonPicoWSActivation { +func bootstrapInitializeComponents(ctx context.Context, config *bootstrapConfig, e Hardware) NonPicoWSActivation { if config.onPreInitializeComponentsFn != nil { config.onPreInitializeComponentsFn(e) } @@ -130,9 +133,9 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) nonPicoWS enableDisplayLogger(e) } - var nonPicoWSApi nonPicoWSActivation - if config.enableNonPicoWebSocket && activateNonPicoWebSocket != nil { - nonPicoWSApi = activateNonPicoWebSocket(e) + var nonPicoWSApi NonPicoWSActivation + if config.enableNonPicoWebSocket { + nonPicoWSApi = ActivateNonPicoWS(ctx, e) } if config.initRandom { @@ -141,7 +144,7 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) nonPicoWS // ui initializaiton is always last if config.uiConfig.ui != nil { - enableUI(e, config.uiConfig) + enableUI(ctx, e, config.uiConfig) } if config.onPostInitializeComponentsFn != nil { @@ -151,7 +154,7 @@ func bootstrapInitializeComponents(config *bootstrapConfig, e *EuroPi) nonPicoWS return nonPicoWSApi } -func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { +func bootstrapRunLoop(config *bootstrapConfig, e Hardware) { if config.appConfig.onAppStartFn != nil { config.appConfig.onAppStartFn(e) } @@ -171,7 +174,7 @@ func bootstrapRunLoop(config *bootstrapConfig, e *EuroPi) { } } -func bootstrapRunLoopWithDelay(config *bootstrapConfig, e *EuroPi) { +func bootstrapRunLoopWithDelay(config *bootstrapConfig, e Hardware) { if config.appConfig.onAppMainLoopFn == nil { panic(errors.New("no main loop specified")) } @@ -192,7 +195,7 @@ func bootstrapRunLoopWithDelay(config *bootstrapConfig, e *EuroPi) { } } -func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { +func bootstrapRunLoopNoDelay(config *bootstrapConfig, e Hardware) { if config.appConfig.onAppMainLoopFn == nil { panic(errors.New("no main loop specified")) } @@ -211,7 +214,7 @@ func bootstrapRunLoopNoDelay(config *bootstrapConfig, e *EuroPi) { } } -func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, nonPicoWSApi nonPicoWSActivation, reason any) { +func bootstrapDestroy(config *bootstrapConfig, e Hardware, nonPicoWSApi NonPicoWSActivation, reason any) { if config.onBeginDestroyFn != nil { config.onBeginDestroyFn(e, reason) } @@ -226,9 +229,9 @@ func bootstrapDestroy(config *bootstrapConfig, e *EuroPi, nonPicoWSApi nonPicoWS uninitRandom(e) - if e != nil && e.Display != nil { + if display := Display(e); display != nil { // show the last buffer - _ = e.Display.Display() + _ = display.Display() } close(piWantDestroyChan) diff --git a/bootstrap_features.go b/bootstrap_features.go index 0abb6a8..34d7112 100644 --- a/bootstrap_features.go +++ b/bootstrap_features.go @@ -1,6 +1,7 @@ package europi import ( + "context" "log" "os" @@ -12,44 +13,57 @@ var ( dispLog displaylogger.Logger ) -func enableDisplayLogger(e *EuroPi) { +func enableDisplayLogger(e Hardware) { if dispLog != nil { // already enabled - can happen when panicking return } + display := Display(e) + if display == nil { + // no display, can't continue + return + } + log.SetFlags(0) - dispLog = displaylogger.NewLogger(e.Display) + dispLog = displaylogger.NewLogger(display) log.SetOutput(dispLog) } -func disableDisplayLogger(e *EuroPi) { +func disableDisplayLogger(e Hardware) { flushDisplayLogger(e) dispLog = nil log.SetOutput(os.Stdout) } -func flushDisplayLogger(e *EuroPi) { +func flushDisplayLogger(e Hardware) { if dispLog != nil { dispLog.Flush() } } -func initRandom(e *EuroPi) { - if e.RND != nil { - _ = e.RND.Configure(hal.RandomGeneratorConfig{}) +func initRandom(e Hardware) { + if rnd := e.Random(); rnd != nil { + _ = rnd.Configure(hal.RandomGeneratorConfig{}) } } -func uninitRandom(e *EuroPi) { +func uninitRandom(e Hardware) { } // used for non-pico testing of bootstrapped europi apps var ( - activateNonPicoWebSocket func(e *EuroPi) nonPicoWSActivation - deactivateNonPicoWebSocket func(e *EuroPi, api nonPicoWSActivation) + activateNonPicoWebSocket func(ctx context.Context, e Hardware) NonPicoWSActivation + deactivateNonPicoWebSocket func(e Hardware, api NonPicoWSActivation) ) -type nonPicoWSActivation interface { +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/bootstrap_lifecycle.go b/bootstrap_lifecycle.go index fa4273f..71fbd50 100644 --- a/bootstrap_lifecycle.go +++ b/bootstrap_lifecycle.go @@ -2,28 +2,32 @@ package europi import "time" -func DefaultPostBootstrapInitialization(e *EuroPi) { - if e.Display == nil { +func DefaultPostBootstrapInitialization(e Hardware) { + display := Display(e) + if display == nil { + // no display, can't continue return } - e.Display.ClearBuffer() - if err := e.Display.Display(); err != nil { + display.ClearBuffer() + if err := display.Display(); err != nil { panic(err) } } -func DefaultBootstrapCompleted(e *EuroPi) { - if e.Display == nil { +func DefaultBootstrapCompleted(e Hardware) { + display := Display(e) + if display == nil { + // no display, can't continue return } - e.Display.ClearBuffer() - if err := e.Display.Display(); err != nil { + display.ClearBuffer() + if err := display.Display(); err != nil { panic(err) } } // DefaultMainLoop is the default main loop used if a new one is not specified to Bootstrap() -func DefaultMainLoop(e *EuroPi, deltaTime time.Duration) { +func DefaultMainLoop(e Hardware, deltaTime time.Duration) { } diff --git a/bootstrap_nonpico.go b/bootstrap_nonpico.go index 5828ef5..ece44b8 100644 --- a/bootstrap_nonpico.go +++ b/bootstrap_nonpico.go @@ -3,14 +3,18 @@ package europi -import "github.com/awonak/EuroPiGo/internal/nonpico" +import ( + "context" -func nonPicoActivateWebSocket(e *EuroPi) nonPicoWSActivation { - nonPicoWSApi := nonpico.ActivateWebSocket(e.Revision) + "github.com/awonak/EuroPiGo/internal/nonpico" +) + +func nonPicoActivateWebSocket(ctx context.Context, e Hardware) NonPicoWSActivation { + nonPicoWSApi := nonpico.ActivateWebSocket(ctx, e.Revision()) return nonPicoWSApi } -func nonPicoDeactivateWebSocket(e *EuroPi, nonPicoWSApi nonPicoWSActivation) { +func nonPicoDeactivateWebSocket(e Hardware, nonPicoWSApi NonPicoWSActivation) { if nonPicoWSApi != nil { if err := nonPicoWSApi.Shutdown(); err != nil { panic(err) diff --git a/bootstrap_nonpico_panic.go b/bootstrap_nonpico_panic.go new file mode 100644 index 0000000..1437632 --- /dev/null +++ b/bootstrap_nonpico_panic.go @@ -0,0 +1,8 @@ +//go:build !pico +// +build !pico + +package europi + +func init() { + DefaultPanicHandler = handlePanicLogger +} diff --git a/bootstrap_panic.go b/bootstrap_panic.go index 0a29022..d82c6cb 100644 --- a/bootstrap_panic.go +++ b/bootstrap_panic.go @@ -12,14 +12,14 @@ import ( // DefaultPanicHandler is the default handler for panics // This will be set by the build flag `onscreenpanic` to `handlePanicOnScreenLog` // Not setting the build flag will set it to `handlePanicDisplayCrash` -var DefaultPanicHandler func(e *EuroPi, reason any) +var DefaultPanicHandler func(e Hardware, reason any) var ( // silence linter _ = handlePanicOnScreenLog ) -func handlePanicOnScreenLog(e *EuroPi, reason any) { +func handlePanicOnScreenLog(e Hardware, reason any) { if e == nil { // can't do anything if it's not enabled return @@ -36,24 +36,19 @@ func handlePanicOnScreenLog(e *EuroPi, reason any) { os.Exit(1) } -func handlePanicLogger(e *EuroPi, reason any) { +func handlePanicLogger(e Hardware, reason any) { log.Panic(reason) } -func handlePanicDisplayCrash(e *EuroPi, reason any) { - if e == nil { - // can't do anything if it's not enabled - return - } - - disp := e.Display - if disp == nil { +func handlePanicDisplayCrash(e Hardware, reason any) { + display := Display(e) + if display == nil { // can't do anything if we don't have a display return } // display a diagonal line pattern through the screen to show that the EuroPi is crashed - width, height := disp.Size() + width, height := display.Size() ymax := height - 1 for x := -ymax; x < width; x += 4 { lx, ly := x, int16(0) @@ -61,6 +56,6 @@ func handlePanicDisplayCrash(e *EuroPi, reason any) { lx = 0 ly = -x } - tinydraw.Line(e.Display, lx, ly, x+ymax, ymax, draw.White) + tinydraw.Line(display, lx, ly, x+ymax, ymax, draw.White) } } diff --git a/bootstrap_panicdisabled.go b/bootstrap_pico_panicdisabled.go similarity index 85% rename from bootstrap_panicdisabled.go rename to bootstrap_pico_panicdisabled.go index 6be5ff3..0d5db92 100644 --- a/bootstrap_panicdisabled.go +++ b/bootstrap_pico_panicdisabled.go @@ -1,5 +1,5 @@ -//go:build !onscreenpanic -// +build !onscreenpanic +//go:build pico && !onscreenpanic +// +build pico,!onscreenpanic package europi diff --git a/bootstrap_panicenabled.go b/bootstrap_pico_panicenabled.go similarity index 56% rename from bootstrap_panicenabled.go rename to bootstrap_pico_panicenabled.go index 39bd62d..87a83ec 100644 --- a/bootstrap_panicenabled.go +++ b/bootstrap_pico_panicenabled.go @@ -1,5 +1,5 @@ -//go:build onscreenpanic -// +build onscreenpanic +//go:build pico && onscreenpanic +// +build pico,onscreenpanic package europi diff --git a/bootstrap_ui.go b/bootstrap_ui.go index c4d861c..55be7e4 100644 --- a/bootstrap_ui.go +++ b/bootstrap_ui.go @@ -1,61 +1,62 @@ package europi import ( + "context" "time" ) -type UserInterface interface { - Start(e *EuroPi) - Paint(e *EuroPi, deltaTime time.Duration) +type UserInterface[THardware Hardware] interface { + Start(e THardware) + Paint(e THardware, deltaTime time.Duration) } -type UserInterfaceLogoPainter interface { - PaintLogo(e *EuroPi, deltaTime time.Duration) +type UserInterfaceLogoPainter[THardware Hardware] interface { + PaintLogo(e THardware, deltaTime time.Duration) } -type UserInterfaceButton1 interface { - Button1(e *EuroPi, deltaTime time.Duration) +type UserInterfaceButton1[THardware Hardware] interface { + Button1(e THardware, deltaTime time.Duration) } type UserInterfaceButton1Debounce interface { Button1Debounce() time.Duration } -type UserInterfaceButton1Ex interface { - Button1Ex(e *EuroPi, value bool, deltaTime time.Duration) +type UserInterfaceButton1Ex[THardware Hardware] interface { + Button1Ex(e THardware, value bool, deltaTime time.Duration) } -type UserInterfaceButton1Long interface { - Button1Long(e *EuroPi, deltaTime time.Duration) +type UserInterfaceButton1Long[THardware Hardware] interface { + Button1Long(e THardware, deltaTime time.Duration) } -type UserInterfaceButton2 interface { - Button2(e *EuroPi, deltaTime time.Duration) +type UserInterfaceButton2[THardware Hardware] interface { + Button2(e THardware, deltaTime time.Duration) } type UserInterfaceButton2Debounce interface { Button2Debounce() time.Duration } -type UserInterfaceButton2Ex interface { - Button2Ex(e *EuroPi, value bool, deltaTime time.Duration) +type UserInterfaceButton2Ex[THardware Hardware] interface { + Button2Ex(e THardware, value bool, deltaTime time.Duration) } -type UserInterfaceButton2Long interface { - Button2Long(e *EuroPi, deltaTime time.Duration) +type UserInterfaceButton2Long[THardware Hardware] interface { + Button2Long(e THardware, deltaTime time.Duration) } var ( ui uiModule ) -func enableUI(e *EuroPi, config bootstrapUIConfig) { +func enableUI(ctx context.Context, e Hardware, config bootstrapUIConfig) { ui.setup(e, config.ui) - ui.start(e, config.uiRefreshRate) + ui.start(ctx, e, config.uiRefreshRate) } -func startUI(e *EuroPi) { +func startUI(e Hardware) { if ui.screen == nil { return } @@ -64,10 +65,10 @@ func startUI(e *EuroPi) { } // ForceRepaintUI schedules a forced repaint of the UI (if it is configured and running) -func ForceRepaintUI(e *EuroPi) { +func ForceRepaintUI(e Hardware) { ui.repaint() } -func disableUI(e *EuroPi) { +func disableUI(e Hardware) { ui.shutdown() } diff --git a/bootstrap_uimodule.go b/bootstrap_uimodule.go index 29cc32e..e320576 100644 --- a/bootstrap_uimodule.go +++ b/bootstrap_uimodule.go @@ -15,28 +15,31 @@ import ( const LongPressDuration = time.Millisecond * 650 type uiModule struct { - screen UserInterface - logoPainter UserInterfaceLogoPainter + screen UserInterface[Hardware] + logoPainter UserInterfaceLogoPainter[Hardware] repaintCh chan struct{} stop context.CancelFunc wg sync.WaitGroup } -func (u *uiModule) setup(e *EuroPi, screen UserInterface) { +func (u *uiModule) setup(e Hardware, screen UserInterface[Hardware]) { + b1 := Button(e, 0) + b2 := Button(e, 1) + ui.screen = screen if ui.screen == nil { return } - ui.logoPainter, _ = screen.(UserInterfaceLogoPainter) + ui.logoPainter, _ = screen.(UserInterfaceLogoPainter[Hardware]) ui.repaintCh = make(chan struct{}, 1) var ( - inputB1 func(e *EuroPi, value bool, deltaTime time.Duration) - inputB1L func(e *EuroPi, deltaTime time.Duration) + inputB1 func(e Hardware, value bool, deltaTime time.Duration) + inputB1L func(e Hardware, deltaTime time.Duration) ) - if in, ok := screen.(UserInterfaceButton1); ok { + if in, ok := screen.(UserInterfaceButton1[Hardware]); ok { var debounceDelay time.Duration if db, ok := screen.(UserInterfaceButton1Debounce); ok { debounceDelay = db.Button1Debounce() @@ -46,22 +49,22 @@ func (u *uiModule) setup(e *EuroPi, screen UserInterface) { in.Button1(e, deltaTime) } }).Debounce(debounceDelay) - inputB1 = func(e *EuroPi, value bool, deltaTime time.Duration) { + inputB1 = func(e Hardware, value bool, deltaTime time.Duration) { inputDB(value) } - } else if in, ok := screen.(UserInterfaceButton1Ex); ok { + } else if in, ok := screen.(UserInterfaceButton1Ex[Hardware]); ok { inputB1 = in.Button1Ex } - if in, ok := screen.(UserInterfaceButton1Long); ok { + if in, ok := screen.(UserInterfaceButton1Long[Hardware]); ok { inputB1L = in.Button1Long } - ui.setupButton(e, e.B1, inputB1, inputB1L) + ui.setupButton(e, b1, inputB1, inputB1L) var ( - inputB2 func(e *EuroPi, value bool, deltaTime time.Duration) - inputB2L func(e *EuroPi, deltaTime time.Duration) + inputB2 func(e Hardware, value bool, deltaTime time.Duration) + inputB2L func(e Hardware, deltaTime time.Duration) ) - if in, ok := screen.(UserInterfaceButton2); ok { + if in, ok := screen.(UserInterfaceButton2[Hardware]); ok { var debounceDelay time.Duration if db, ok := screen.(UserInterfaceButton2Debounce); ok { debounceDelay = db.Button2Debounce() @@ -71,21 +74,21 @@ func (u *uiModule) setup(e *EuroPi, screen UserInterface) { in.Button2(e, deltaTime) } }).Debounce(debounceDelay) - inputB2 = func(e *EuroPi, value bool, deltaTime time.Duration) { + inputB2 = func(e Hardware, value bool, deltaTime time.Duration) { inputDB(value) } - } else if in, ok := screen.(UserInterfaceButton2Ex); ok { + } else if in, ok := screen.(UserInterfaceButton2Ex[Hardware]); ok { inputB2 = in.Button2Ex } - if in, ok := screen.(UserInterfaceButton2Long); ok { + if in, ok := screen.(UserInterfaceButton2Long[Hardware]); ok { inputB2L = in.Button2Long } - ui.setupButton(e, e.B2, inputB2, inputB2L) + ui.setupButton(e, b2, inputB2, inputB2L) } -func (u *uiModule) start(e *EuroPi, interval time.Duration) { +func (u *uiModule) start(ctx context.Context, e Hardware, interval time.Duration) { ui.wg.Add(1) - go ui.run(e, interval) + go ui.run(ctx, e, interval) } func (u *uiModule) wait() { @@ -110,17 +113,17 @@ func (u *uiModule) shutdown() { ui.wait() } -func (u *uiModule) run(e *EuroPi, interval time.Duration) { +func (u *uiModule) run(ctx context.Context, e Hardware, interval time.Duration) { defer u.wg.Done() - disp := e.Display + disp := Display(e) if disp == nil { // no display means no ui // TODO: make uiModule work when any user input/output is specified, not just display return } - ctx, cancel := context.WithCancel(context.Background()) + myCtx, cancel := context.WithCancel(ctx) ui.stop = cancel defer ui.stop() @@ -139,7 +142,7 @@ func (u *uiModule) run(e *EuroPi, interval time.Duration) { lastTime := time.Now() for { select { - case <-ctx.Done(): + case <-myCtx.Done(): return case <-ui.repaintCh: @@ -156,7 +159,7 @@ func (u *uiModule) run(e *EuroPi, interval time.Duration) { } } -func (u *uiModule) setupButton(e *EuroPi, btn hal.ButtonInput, onShort func(e *EuroPi, value bool, deltaTime time.Duration), onLong func(e *EuroPi, deltaTime time.Duration)) { +func (u *uiModule) setupButton(e Hardware, btn hal.ButtonInput, onShort func(e Hardware, value bool, deltaTime time.Duration), onLong func(e Hardware, deltaTime time.Duration)) { if btn == nil { return } @@ -167,12 +170,12 @@ func (u *uiModule) setupButton(e *EuroPi, btn hal.ButtonInput, onShort func(e *E if onShort == nil { // no-op - onShort = func(e *EuroPi, value bool, deltaTime time.Duration) {} + onShort = func(e Hardware, value bool, deltaTime time.Duration) {} } // if no long-press handler present, just reuse short-press handler if onLong == nil { - onLong = func(e *EuroPi, deltaTime time.Duration) { + onLong = func(e Hardware, deltaTime time.Duration) { onShort(e, false, deltaTime) } } diff --git a/bootstrapoptions.go b/bootstrapoptions.go index f0d5718..89f58b1 100644 --- a/bootstrapoptions.go +++ b/bootstrapoptions.go @@ -4,10 +4,10 @@ package europi type BootstrapOption func(o *bootstrapConfig) error type bootstrapConfig struct { - panicHandler func(e *EuroPi, reason any) + panicHandler func(e Hardware, reason any) enableDisplayLogger bool initRandom bool - europi *EuroPi + europi Hardware enableNonPicoWebSocket bool // application diff --git a/bootstrapoptions_app.go b/bootstrapoptions_app.go index 4b1a3ec..b1d80d8 100644 --- a/bootstrapoptions_app.go +++ b/bootstrapoptions_app.go @@ -5,16 +5,16 @@ import ( "time" ) -type ApplicationStart interface { - Start(e *EuroPi) +type ApplicationStart[THardware Hardware] interface { + Start(e THardware) } -type ApplicationMainLoop interface { - MainLoop(e *EuroPi, deltaTime time.Duration) +type ApplicationMainLoop[THardware Hardware] interface { + MainLoop(e THardware, deltaTime time.Duration) } -type ApplicationEnd interface { - End(e *EuroPi) +type ApplicationEnd[THardware Hardware] interface { + End(e THardware) } // App sets the application handler interface with optional parameters @@ -23,23 +23,17 @@ func App(app any, opts ...BootstrapAppOption) BootstrapOption { if app == nil { return errors.New("app must not be nil") } - start, _ := app.(ApplicationStart) - mainLoop, _ := app.(ApplicationMainLoop) - end, _ := app.(ApplicationEnd) + + // automatically divine the functions for the app + start, mainLoop, end := getAppFuncs(app) if start == nil && mainLoop == nil && end == nil { return errors.New("app must provide at least one application function interface (ApplicationStart, ApplicationMainLoop, ApplicationEnd)") } - if start != nil { - o.appConfig.onAppStartFn = start.Start - } - if mainLoop != nil { - o.appConfig.onAppMainLoopFn = mainLoop.MainLoop - } - if end != nil { - o.appConfig.onAppEndFn = end.End - } + o.appConfig.onAppStartFn = start + o.appConfig.onAppMainLoopFn = mainLoop + o.appConfig.onAppEndFn = end o.appConfig.options = opts return nil diff --git a/bootstrapoptions_app_conversion.go b/bootstrapoptions_app_conversion.go new file mode 100644 index 0000000..aeae862 --- /dev/null +++ b/bootstrapoptions_app_conversion.go @@ -0,0 +1,111 @@ +package europi + +import ( + "time" +) + +// appHardwareWrapper sets up a wrapper around an app that expects a particular hardware interface +// this is for automated parameter interpretation +func appHardwareWrapper[THardware Hardware](app any) any { + start, _ := app.(ApplicationStart[THardware]) + mainLoop, _ := app.(ApplicationMainLoop[THardware]) + end, _ := app.(ApplicationEnd[THardware]) + return &appWrapper[THardware]{ + start: start, + mainLoop: mainLoop, + end: end, + } +} + +func getAppFuncs(app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { + if appStart, _ := app.(ApplicationStart[Hardware]); appStart != nil { + start = appStart.Start + } + if appMainLoop, _ := app.(ApplicationMainLoop[Hardware]); appMainLoop != nil { + mainLoop = appMainLoop.MainLoop + } + if appEnd, _ := app.(ApplicationEnd[Hardware]); appEnd != nil { + end = appEnd.End + } + + if start == nil && mainLoop == nil && end == nil { + start, mainLoop, end = getWrappedAppFuncs[*EuroPiPrototype](app) + } + + if start == nil && mainLoop == nil && end == nil { + start, mainLoop, end = getWrappedAppFuncs[*EuroPi](app) + } + return +} + +func getWrappedAppFuncs[THardware Hardware](app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { + appWrapper := appHardwareWrapper[THardware](app) + if getStart, _ := appWrapper.(applicationStartProvider); getStart != nil { + start = getStart.ApplicationStart() + } + + if getMainLoop, _ := appWrapper.(applicationMainLoopProvider); getMainLoop != nil { + mainLoop = getMainLoop.ApplicationMainLoop() + } + + if getEnd, _ := appWrapper.(applicationEndProvider); getEnd != nil { + end = getEnd.ApplicationEnd() + } + return +} + +type applicationStartProvider interface { + ApplicationStart() AppStartFunc +} + +type applicationMainLoopProvider interface { + ApplicationMainLoop() AppMainLoopFunc +} + +type applicationEndProvider interface { + ApplicationEnd() AppEndFunc +} + +type appWrapper[THardware Hardware] struct { + start ApplicationStart[THardware] + mainLoop ApplicationMainLoop[THardware] + end ApplicationEnd[THardware] +} + +func (a *appWrapper[THardware]) ApplicationStart() AppStartFunc { + if a.start == nil { + return nil + } + return a.doStart +} + +func (a *appWrapper[THardware]) doStart(e Hardware) { + pi, _ := e.(THardware) + a.start.Start(pi) +} + +func (a *appWrapper[THardware]) ApplicationMainLoop() AppMainLoopFunc { + if a.mainLoop == nil { + return nil + } + return a.doMainLoop +} + +func (a *appWrapper[THardware]) doMainLoop(e Hardware, deltaTime time.Duration) { + pi, _ := e.(THardware) + a.mainLoop.MainLoop(pi, deltaTime) +} + +func (a *appWrapper[THardware]) ApplicationEnd() AppEndFunc { + if a.end == nil { + return nil + } + return a.doEnd +} + +func (a *appWrapper[THardware]) doEnd(e Hardware) { + if a.end != nil { + pi, _ := e.(THardware) + a.end.End(pi) + } +} diff --git a/bootstrapoptions_features.go b/bootstrapoptions_features.go index 8e88268..7c350fe 100644 --- a/bootstrapoptions_features.go +++ b/bootstrapoptions_features.go @@ -35,7 +35,7 @@ func InitRandom(enabled bool) BootstrapOption { } // UsingEuroPi sets a specific EuroPi object instance for all operations in the bootstrap -func UsingEuroPi(e *EuroPi) BootstrapOption { +func UsingEuroPi(e Hardware) BootstrapOption { return func(o *bootstrapConfig) error { if e == nil { return errors.New("europi instance must not be nil") diff --git a/bootstrapoptions_lifecycle.go b/bootstrapoptions_lifecycle.go index 401e8fc..a03d7d7 100644 --- a/bootstrapoptions_lifecycle.go +++ b/bootstrapoptions_lifecycle.go @@ -49,15 +49,15 @@ Bootstrap: destroyBootstrap */ type ( - PostBootstrapConstructionFunc func(e *EuroPi) - PreInitializeComponentsFunc func(e *EuroPi) - PostInitializeComponentsFunc func(e *EuroPi) - BootstrapCompletedFunc func(e *EuroPi) - AppStartFunc func(e *EuroPi) - AppMainLoopFunc func(e *EuroPi, deltaTime time.Duration) - AppEndFunc func(e *EuroPi) - BeginDestroyFunc func(e *EuroPi, reason any) - FinishDestroyFunc func(e *EuroPi) + PostBootstrapConstructionFunc func(e Hardware) + PreInitializeComponentsFunc func(e Hardware) + PostInitializeComponentsFunc func(e Hardware) + BootstrapCompletedFunc func(e Hardware) + AppStartFunc func(e Hardware) + AppMainLoopFunc func(e Hardware, deltaTime time.Duration) + AppEndFunc func(e Hardware) + BeginDestroyFunc func(e Hardware, reason any) + FinishDestroyFunc func(e Hardware) ) // PostBootstrapConstruction sets the function that runs immediately after primary EuroPi bootstrap diff --git a/bootstrapoptions_ui.go b/bootstrapoptions_ui.go index aa40a5e..ce6da0c 100644 --- a/bootstrapoptions_ui.go +++ b/bootstrapoptions_ui.go @@ -6,7 +6,7 @@ import ( ) // UI sets the user interface handler interface -func UI(ui UserInterface, opts ...BootstrapUIOption) BootstrapOption { +func UI(ui UserInterface[Hardware], opts ...BootstrapUIOption) BootstrapOption { return func(o *bootstrapConfig) error { if ui == nil { return errors.New("ui must not be nil") @@ -25,7 +25,7 @@ const ( type BootstrapUIOption func(o *bootstrapUIConfig) error type bootstrapUIConfig struct { - ui UserInterface + ui UserInterface[Hardware] uiRefreshRate time.Duration options []BootstrapUIOption diff --git a/europi.go b/europi.go index 7950f51..2dcfd52 100644 --- a/europi.go +++ b/europi.go @@ -3,38 +3,30 @@ package europi // import "github.com/awonak/EuroPiGo" import ( "github.com/awonak/EuroPiGo/hardware" "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev0" + "github.com/awonak/EuroPiGo/hardware/rev1" ) -// EuroPi is the collection of component wrappers used to interact with the module. -type EuroPi struct { - Revision hal.Revision +type ( + // Hardware is the collection of component wrappers used to interact with the module. + Hardware = hal.Hardware - Display hal.DisplayOutput - DI hal.DigitalInput - AI hal.AnalogInput - B1 hal.ButtonInput - B2 hal.ButtonInput - K1 hal.KnobInput - K2 hal.KnobInput - CV1 hal.VoltageOutput - CV2 hal.VoltageOutput - CV3 hal.VoltageOutput - CV4 hal.VoltageOutput - CV5 hal.VoltageOutput - CV6 hal.VoltageOutput - CV [6]hal.VoltageOutput - RND hal.RandomGenerator -} + // EuroPiPrototype is the revision 0 hardware + EuroPiPrototype = rev0.EuroPiPrototype + // EuroPi is the revision 1 hardware + EuroPi = rev1.EuroPi + // TODO: add rev2 +) // New will return a new EuroPi struct based on the detected hardware revision -func New() *EuroPi { +func New() Hardware { // 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) *EuroPi { +func NewFrom(revision hal.Revision) Hardware { if revision == hal.RevisionUnknown { // unknown revision return nil @@ -43,36 +35,69 @@ func NewFrom(revision hal.Revision) *EuroPi { // this will block until the hardware components are initialized hardware.WaitForReady() - cv1 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage1Output) - cv2 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage2Output) - cv3 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage3Output) - cv4 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage4Output) - cv5 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage5Output) - cv6 := hardware.GetHardware[hal.VoltageOutput](revision, hal.HardwareIdVoltage6Output) - - e := &EuroPi{ - Revision: revision, + 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 + } +} - Display: hardware.GetHardware[hal.DisplayOutput](revision, hal.HardwareIdDisplay1Output), +// Display returns the primary display from the hardware interface, if it has one +func Display(e Hardware) hal.DisplayOutput { + if e == nil { + return nil + } - DI: hardware.GetHardware[hal.DigitalInput](revision, hal.HardwareIdDigital1Input), - AI: hardware.GetHardware[hal.AnalogInput](revision, hal.HardwareIdAnalog1Input), + switch e.Revision() { + case hal.Revision1: + return e.(*rev1.EuroPi).OLED + case hal.Revision2: + // TODO: add rev2 + //return e.(*rev2.EuroPiX).Display + } + return nil +} - B1: hardware.GetHardware[hal.ButtonInput](revision, hal.HardwareIdButton1Input), - B2: hardware.GetHardware[hal.ButtonInput](revision, hal.HardwareIdButton2Input), +// 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 + } - K1: hardware.GetHardware[hal.KnobInput](revision, hal.HardwareIdKnob1Input), - K2: hardware.GetHardware[hal.KnobInput](revision, hal.HardwareIdKnob2Input), + 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 +} - CV1: cv1, - CV2: cv2, - CV3: cv3, - CV4: cv4, - CV5: cv5, - CV6: cv6, - CV: [6]hal.VoltageOutput{cv1, cv2, cv3, cv4, cv5, cv6}, - RND: hardware.GetHardware[hal.RandomGenerator](revision, hal.HardwareIdRandom1Generator), +// 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 } - return e + 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 } diff --git a/europi_test.go b/europi_test.go index 85f632d..7e68c14 100644 --- a/europi_test.go +++ b/europi_test.go @@ -4,20 +4,48 @@ 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.Fatal("EuroPi New: expected[nil] actual[non-nil]") + t.Fatalf("EuroPi New: expected[nil] actual[%T]", actual) + } + }) + + t.Run("Revision0", func(t *testing.T) { + hardware.SetDetectedRevision(hal.Revision0) + if actual, _ := europi.New().(*rev0.EuroPiPrototype); actual == nil { + t.Fatalf("EuroPi New: expected[EuroPiPrototype] actual[%T]", actual) + } + }) + + t.Run("Revision1", func(t *testing.T) { + hardware.SetDetectedRevision(hal.Revision1) + if actual, _ := europi.New().(*rev1.EuroPi); actual == nil { + t.Fatalf("EuroPi New: expected[EuroPi] actual[%T]", 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) { - if actual := europi.NewFrom(hal.Revision1); actual == nil { - t.Fatal("EuroPi New: expected[non-nil] actual[nil]") + 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/experimental/envelope/map.go b/experimental/envelope/map.go index 9c1249a..2e9fa7f 100644 --- a/experimental/envelope/map.go +++ b/experimental/envelope/map.go @@ -4,8 +4,14 @@ import "github.com/awonak/EuroPiGo/lerp" type Map[TIn, TOut lerp.Lerpable] interface { Remap(value TIn) TOut + Unmap(value TOut) TIn InputMinimum() TIn InputMaximum() TIn OutputMinimum() TOut OutputMaximum() TOut } + +type remapList[TIn, TOut lerp.Lerpable, TFloat lerp.Float] struct { + lerp.Remapper[TIn, TOut, TFloat] + nextOut *remapList[TIn, TOut, TFloat] +} diff --git a/experimental/envelope/map32.go b/experimental/envelope/map32.go index aee3524..d6442f2 100644 --- a/experimental/envelope/map32.go +++ b/experimental/envelope/map32.go @@ -7,8 +7,9 @@ import ( ) type envMap32[TIn, TOut lerp.Lerpable] struct { - rem []lerp.Remapper32[TIn, TOut] - outMax TOut + rem []remapList[TIn, TOut, float32] + outMax TOut + outRoot *remapList[TIn, TOut, float32] } func NewMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { @@ -22,22 +23,37 @@ func NewMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TO // ensure it's sorted sort.Sort(p) - var outMax TOut - var rem []lerp.Remapper32[TIn, TOut] - if len(p) == 1 { - cur := p[0] - outMax = cur.Output - rem = append(rem, lerp.NewRemapPoint[TIn, TOut, float32](cur.Input, cur.Output)) - } else { + var rem []remapList[TIn, TOut, float32] + if len(p) > 1 { for pos := 0; pos < len(p)-1; pos++ { cur, next := p[pos], p[pos+1] - outMax = next.Output - rem = append(rem, lerp.NewRemap32(cur.Input, next.Input, cur.Output, next.Output)) + rem = append(rem, remapList[TIn, TOut, float32]{ + Remapper: lerp.NewRemap32(cur.Input, next.Input, cur.Output, next.Output), + }) } } + last := &p[len(p)-1] + rem = append(rem, remapList[TIn, TOut, float32]{ + Remapper: lerp.NewRemapPoint[TIn, TOut, float32](last.Input, last.Output), + }) + + outSort := make(MapEntryList[TOut, int], len(rem)) + for i, e := range rem { + outSort[i].Input = e.OutputMinimum() + outSort[i].Output = i + } + sort.Sort(outSort) + rootIdx := outSort[0].Output + outRoot := &rem[rootIdx] + for pos := 0; pos < len(rem)-1; pos++ { + cur, next := outSort[pos].Output, outSort[pos+1].Output + rem[cur].nextOut = &rem[next] + } + return &envMap32[TIn, TOut]{ - rem: rem, - outMax: outMax, + rem: rem, + outMax: last.Output, + outRoot: outRoot, } } @@ -53,6 +69,28 @@ func (m *envMap32[TIn, TOut]) Remap(value TIn) TOut { return m.outMax } +func (m *envMap32[TIn, TOut]) Unmap(value TOut) TIn { + for r := m.outRoot; r != nil; r = r.nextOut { + outMin := r.OutputMinimum() + outMax := r.OutputMaximum() + if outMin < outMax { + if value < outMin { + return r.InputMinimum() + } else if value < outMax { + return r.Unmap(value) + } + } else { + if value < outMax { + return r.InputMinimum() + } else if value < outMin { + return r.Unmap(value) + } + } + } + + return m.InputMaximum() +} + func (m *envMap32[TIn, TOut]) InputMinimum() TIn { // we're guaranteed to have 1 point return m.rem[0].InputMaximum() diff --git a/experimental/envelope/map64.go b/experimental/envelope/map64.go index 0e687b2..3d5aaed 100644 --- a/experimental/envelope/map64.go +++ b/experimental/envelope/map64.go @@ -7,8 +7,9 @@ import ( ) type envMap64[TIn, TOut lerp.Lerpable] struct { - rem []lerp.Remapper64[TIn, TOut] - outMax TOut + rem []remapList[TIn, TOut, float64] + outMax TOut + outRoot *remapList[TIn, TOut, float64] } func NewMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { @@ -22,22 +23,37 @@ func NewMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TO // ensure it's sorted sort.Sort(p) - var outMax TOut - var rem []lerp.Remapper64[TIn, TOut] - if len(p) == 1 { - cur := p[0] - outMax = cur.Output - rem = append(rem, lerp.NewRemapPoint[TIn, TOut, float64](cur.Input, cur.Output)) - } else { + var rem []remapList[TIn, TOut, float64] + if len(p) > 1 { for pos := 0; pos < len(p)-1; pos++ { cur, next := p[pos], p[pos+1] - outMax = next.Output - rem = append(rem, lerp.NewRemap64(cur.Input, next.Input, cur.Output, next.Output)) + rem = append(rem, remapList[TIn, TOut, float64]{ + Remapper: lerp.NewRemap64(cur.Input, next.Input, cur.Output, next.Output), + }) } } + last := &p[len(p)-1] + rem = append(rem, remapList[TIn, TOut, float64]{ + Remapper: lerp.NewRemapPoint[TIn, TOut, float64](last.Input, last.Output), + }) + + outSort := make(MapEntryList[TOut, int], len(rem)) + for i, e := range rem { + outSort[i].Input = e.OutputMinimum() + outSort[i].Output = i + } + sort.Sort(outSort) + rootIdx := outSort[0].Output + outRoot := &rem[rootIdx] + for pos := 0; pos < len(rem)-1; pos++ { + cur, next := outSort[pos].Output, outSort[pos+1].Output + rem[cur].nextOut = &rem[next] + } + return &envMap64[TIn, TOut]{ - rem: rem, - outMax: outMax, + rem: rem, + outMax: last.Output, + outRoot: outRoot, } } @@ -53,6 +69,28 @@ func (m *envMap64[TIn, TOut]) Remap(value TIn) TOut { return m.outMax } +func (m *envMap64[TIn, TOut]) Unmap(value TOut) TIn { + for r := m.outRoot; r != nil; r = r.nextOut { + outMin := r.OutputMinimum() + outMax := r.OutputMaximum() + if outMin < outMax { + if value < outMin { + return r.InputMinimum() + } else if value < outMax { + return r.Unmap(value) + } + } else { + if value < outMax { + return r.InputMinimum() + } else if value < outMin { + return r.Unmap(value) + } + } + } + + return m.InputMaximum() +} + func (m *envMap64[TIn, TOut]) InputMinimum() TIn { // we're guaranteed to have 1 point return m.rem[0].InputMaximum() diff --git a/experimental/knobmenu/knobmenu.go b/experimental/knobmenu/knobmenu.go index 1f40183..b2621da 100644 --- a/experimental/knobmenu/knobmenu.go +++ b/experimental/knobmenu/knobmenu.go @@ -71,10 +71,13 @@ func (m *KnobMenu) Next() { m.kb.Next() } -func (m *KnobMenu) Paint(e *europi.EuroPi, deltaTime time.Duration) { +func (m *KnobMenu) Paint(e europi.Hardware, deltaTime time.Duration) { m.updateMenu(e) - m.writer.Display = e.Display + m.writer.Display = europi.Display(e) + if m.writer.Display == nil { + return + } y := m.y selectedIdx := m.kb.CurrentIndex() - 1 @@ -93,7 +96,7 @@ func (m *KnobMenu) Paint(e *europi.EuroPi, deltaTime time.Duration) { } } -func (m *KnobMenu) updateMenu(e *europi.EuroPi) { +func (m *KnobMenu) updateMenu(e europi.Hardware) { cur := m.kb.CurrentName() for _, it := range m.items { if it.name == cur { diff --git a/experimental/screenbank/screenbank.go b/experimental/screenbank/screenbank.go index be04243..f647bb3 100644 --- a/experimental/screenbank/screenbank.go +++ b/experimental/screenbank/screenbank.go @@ -42,7 +42,7 @@ func (sb *ScreenBank) CurrentName() string { return sb.bank[sb.current].name } -func (sb *ScreenBank) Current() europi.UserInterface { +func (sb *ScreenBank) Current() *screenBankEntryDetails { if len(sb.bank) == 0 { return nil } @@ -80,7 +80,7 @@ func (sb *ScreenBank) Next() { sb.transitionTo(sb.current + 1) } -func (sb *ScreenBank) Start(e *europi.EuroPi) { +func (sb *ScreenBank) Start(e europi.Hardware) { for i := range sb.bank { s := &sb.bank[i] @@ -91,21 +91,22 @@ func (sb *ScreenBank) Start(e *europi.EuroPi) { } } -func (sb *ScreenBank) PaintLogo(e *europi.EuroPi, deltaTime time.Duration) { - if sb.current >= len(sb.bank) { +func (sb *ScreenBank) PaintLogo(e europi.Hardware, deltaTime time.Duration) { + display := europi.Display(e) + if sb.current >= len(sb.bank) || display == nil { return } cur := &sb.bank[sb.current] cur.lock() if cur.logo != "" { - sb.writer.Display = e.Display + sb.writer.Display = display sb.writer.WriteLineInverseAligned(cur.logo, 0, 16, draw.White, fontwriter.AlignRight, fontwriter.AlignMiddle) } cur.unlock() } -func (sb *ScreenBank) Paint(e *europi.EuroPi, deltaTime time.Duration) { +func (sb *ScreenBank) Paint(e europi.Hardware, deltaTime time.Duration) { if sb.current >= len(sb.bank) { return } @@ -118,19 +119,20 @@ func (sb *ScreenBank) Paint(e *europi.EuroPi, deltaTime time.Duration) { cur.unlock() } -func (sb *ScreenBank) Button1Ex(e *europi.EuroPi, value bool, deltaTime time.Duration) { +func (sb *ScreenBank) Button1Ex(e europi.Hardware, value bool, deltaTime time.Duration) { screen := sb.Current() - if cur, ok := screen.(europi.UserInterfaceButton1); ok { + if cur := screen.UserInterfaceButton1; cur != nil { if !value { cur.Button1(e, deltaTime) } - } else if cur, ok := screen.(europi.UserInterfaceButton1Ex); ok { + } else if cur := screen.UserInterfaceButton1Ex; cur != nil { cur.Button1Ex(e, value, deltaTime) } } -func (sb *ScreenBank) Button1Long(e *europi.EuroPi, deltaTime time.Duration) { - if cur, ok := sb.Current().(europi.UserInterfaceButton1Long); ok { +func (sb *ScreenBank) Button1Long(e europi.Hardware, deltaTime time.Duration) { + screen := sb.Current() + if cur := screen.UserInterfaceButton1Long; cur != nil { cur.Button1Long(e, deltaTime) } else { // try the short-press @@ -138,17 +140,17 @@ func (sb *ScreenBank) Button1Long(e *europi.EuroPi, deltaTime time.Duration) { } } -func (sb *ScreenBank) Button2Ex(e *europi.EuroPi, value bool, deltaTime time.Duration) { +func (sb *ScreenBank) Button2Ex(e europi.Hardware, value bool, deltaTime time.Duration) { screen := sb.Current() - if cur, ok := screen.(europi.UserInterfaceButton2); ok { + if cur := screen.UserInterfaceButton2; cur != nil { if !value { cur.Button2(e, deltaTime) } - } else if cur, ok := screen.(europi.UserInterfaceButton2Ex); ok { + } else if cur := screen.UserInterfaceButton2Ex; cur != nil { cur.Button2Ex(e, value, deltaTime) } } -func (sb *ScreenBank) Button2Long(e *europi.EuroPi, deltaTime time.Duration) { +func (sb *ScreenBank) Button2Long(e europi.Hardware, deltaTime time.Duration) { sb.Next() } diff --git a/experimental/screenbank/screenbankentry.go b/experimental/screenbank/screenbankentry.go index e3fdf10..deda8a4 100644 --- a/experimental/screenbank/screenbankentry.go +++ b/experimental/screenbank/screenbankentry.go @@ -9,7 +9,7 @@ import ( type screenBankEntry struct { name string logo string - screen europi.UserInterface + screen *screenBankEntryDetails enabled bool locked bool lastUpdate time.Time @@ -30,3 +30,12 @@ func (e *screenBankEntry) unlock() { e.locked = false } + +type screenBankEntryDetails struct { + europi.UserInterface[europi.Hardware] + europi.UserInterfaceButton1[europi.Hardware] + europi.UserInterfaceButton1Long[europi.Hardware] + europi.UserInterfaceButton1Ex[europi.Hardware] + europi.UserInterfaceButton2[europi.Hardware] + europi.UserInterfaceButton2Ex[europi.Hardware] +} diff --git a/experimental/screenbank/screenbankoptions.go b/experimental/screenbank/screenbankoptions.go index 22aefa3..8e7fb19 100644 --- a/experimental/screenbank/screenbankoptions.go +++ b/experimental/screenbank/screenbankoptions.go @@ -1,6 +1,7 @@ package screenbank import ( + "fmt" "time" europi "github.com/awonak/EuroPiGo" @@ -10,12 +11,16 @@ type ScreenBankOption func(sb *ScreenBank) error // WithScreen sets up a new screen in the chain // logo is the emoji to use (see https://github.com/tinygo-org/tinyfont/blob/release/notoemoji/NotoEmoji-Regular-12pt.go) -func WithScreen(name string, logo string, screen europi.UserInterface) ScreenBankOption { +func WithScreen(name string, logo string, screen any) ScreenBankOption { return func(sb *ScreenBank) error { + details := getScreen(screen) + if details == nil { + return fmt.Errorf("screen %q does not implement a variant of europi.UserInterface", name) + } e := screenBankEntry{ name: name, logo: logo, - screen: screen, + screen: details, enabled: true, locked: true, lastUpdate: time.Now(), @@ -25,3 +30,112 @@ func WithScreen(name string, logo string, screen europi.UserInterface) ScreenBan return nil } } + +func getScreen(screen any) *screenBankEntryDetails { + if s, _ := screen.(europi.UserInterface[europi.Hardware]); s != nil { + details := &screenBankEntryDetails{ + UserInterface: s, + } + + details.UserInterfaceButton1, _ = screen.(europi.UserInterfaceButton1[europi.Hardware]) + details.UserInterfaceButton1Long, _ = screen.(europi.UserInterfaceButton1Long[europi.Hardware]) + details.UserInterfaceButton1Ex, _ = screen.(europi.UserInterfaceButton1Ex[europi.Hardware]) + details.UserInterfaceButton2, _ = screen.(europi.UserInterfaceButton2[europi.Hardware]) + details.UserInterfaceButton2Ex, _ = screen.(europi.UserInterfaceButton2Ex[europi.Hardware]) + + return details + } + + if s := getScreenForHardware[*europi.EuroPiPrototype](screen); s != nil { + return s + } + + if s := getScreenForHardware[*europi.EuroPi](screen); s != nil { + return s + } + + // TODO: add rev2 + + return nil +} + +func getScreenForHardware[THardware europi.Hardware](screen any) *screenBankEntryDetails { + s, _ := screen.(europi.UserInterface[THardware]) + if s == nil { + return nil + } + + wrapper := &screenHardwareWrapper[THardware]{ + UserInterface: s, + } + + details := &screenBankEntryDetails{ + UserInterface: wrapper, + } + + if wrapper.button1, _ = screen.(europi.UserInterfaceButton1[THardware]); wrapper.button1 != nil { + details.UserInterfaceButton1 = wrapper + } + if wrapper.button1Long, _ = screen.(europi.UserInterfaceButton1Long[THardware]); wrapper.button1Long != nil { + details.UserInterfaceButton1Long = wrapper + } + if wrapper.button1Ex, _ = screen.(europi.UserInterfaceButton1Ex[THardware]); wrapper.button1Ex != nil { + details.UserInterfaceButton1Ex = wrapper + } + if wrapper.button2, _ = screen.(europi.UserInterfaceButton2[THardware]); wrapper.button2 != nil { + details.UserInterfaceButton2 = wrapper + } + if wrapper.button2Ex, _ = screen.(europi.UserInterfaceButton2Ex[THardware]); wrapper.button2Ex != nil { + details.UserInterfaceButton2Ex = wrapper + } + + return details +} + +type screenHardwareWrapper[THardware europi.Hardware] struct { + europi.UserInterface[THardware] + button1 europi.UserInterfaceButton1[THardware] + button1Ex europi.UserInterfaceButton1Ex[THardware] + button1Long europi.UserInterfaceButton1Long[THardware] + button2 europi.UserInterfaceButton2[THardware] + button2Ex europi.UserInterfaceButton2Ex[THardware] +} + +func (w *screenHardwareWrapper[THardware]) Start(e europi.Hardware) { + pi, _ := e.(THardware) + w.UserInterface.Start(pi) +} + +func (w *screenHardwareWrapper[THardware]) Paint(e europi.Hardware, deltaTime time.Duration) { + pi, _ := e.(THardware) + w.UserInterface.Paint(pi, deltaTime) +} + +func (w *screenHardwareWrapper[THardware]) Button1(e europi.Hardware, deltaTime time.Duration) { + pi, _ := e.(THardware) + w.button1.Button1(pi, deltaTime) +} + +func (w *screenHardwareWrapper[THardware]) Button1Ex(e europi.Hardware, value bool, deltaTime time.Duration) { + pi, _ := e.(THardware) + w.button1Ex.Button1Ex(pi, value, deltaTime) +} + +func (w *screenHardwareWrapper[THardware]) Button1Long(e europi.Hardware, deltaTime time.Duration) { + pi, _ := e.(THardware) + w.button1Long.Button1Long(pi, deltaTime) +} + +func (w *screenHardwareWrapper[THardware]) Button2(e europi.Hardware, deltaTime time.Duration) { + pi, _ := e.(THardware) + w.button2.Button2(pi, deltaTime) +} + +func (w *screenHardwareWrapper[THardware]) Button2Ex(e europi.Hardware, value bool, deltaTime time.Duration) { + pi, _ := e.(THardware) + w.button2Ex.Button2Ex(pi, value, deltaTime) +} + +// Button1Debounce() time.Duration +// Button2Debounce() time.Duration +// Button2Long(e THardware, deltaTime time.Duration) diff --git a/go.mod b/go.mod index c126ef8..894749b 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,8 @@ module github.com/awonak/EuroPiGo go 1.18 require ( + 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 ) - -require github.com/gorilla/websocket v1.5.0 // indirect diff --git a/hardware/README.md b/hardware/README.md index 57983cc..db1517c 100644 --- a/hardware/README.md +++ b/hardware/README.md @@ -18,21 +18,23 @@ This package is used for obtaining singleton objects for particular hardware, id 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 | 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` | The Digital Input of the EuroPi. | -| `HardwareIdAnalog1Input` | `HardwareIdAnalogue1Input` | `hal.AnalogInput` | The Analogue Input of the EuroPi. | -| `HardwareIdDisplay1Output` | | `hal.DisplayOutput` | 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` | The Button 1 gate input of the EuroPi. | -| `HardwareIdButton2Input` | | `hal.ButtonInput` | The Button 2 gate input of the EuroPi. | -| `HardwareIdKnob1Input` | | `hal.KnobInput` | The Knob 1 potentiometer input of the EuroPi. | -| `HardwareIdKnob2Input` | | `hal.KnobInput` | The Knob 2 potentiometer input of the EuroPi. | -| `HardwareIdVoltage1Output` | `HardwareIdCV1Output` | `hal.VoltageOutput` | The #1 `CV` / `V/Octave` output of the EuroPi. While it 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. | -| `HardwareIdVoltage2Output` | `HardwareIdCV2Output` | `hal.VoltageOutput` | The #2 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | -| `HardwareIdVoltage3Output` | `HardwareIdCV3Output` | `hal.VoltageOutput` | The #3 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | -| `HardwareIdVoltage4Output` | `HardwareIdCV4Output` | `hal.VoltageOutput` | The #4 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | -| `HardwareIdVoltage5Output` | `HardwareIdCV5Output` | `hal.VoltageOutput` | The #5 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | -| `HardwareIdVoltage6Output` | `HardwareIdCV6Output` | `hal.VoltageOutput` | The #6 `CV` / `V/Octave` output of the EuroPi. See `HardwareIdVoltage1Output` for more details. | -| `HardwareIdRandom1Generator` | | `hal.RandomGenerator` | Provides an interface to calibrate or seed the random number generator of the hardware. | +| 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..4f0a313 --- /dev/null +++ b/hardware/common/analoginput.go @@ -0,0 +1,103 @@ +package common + +import ( + "errors" + + "github.com/awonak/EuroPiGo/clamp" + "github.com/awonak/EuroPiGo/experimental/envelope" + "github.com/awonak/EuroPiGo/hardware/hal" + "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 envelope.Map[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/rev1/digitalinput.go b/hardware/common/digitalinput.go similarity index 69% rename from hardware/rev1/digitalinput.go rename to hardware/common/digitalinput.go index 506544c..c030ac2 100644 --- a/hardware/rev1/digitalinput.go +++ b/hardware/common/digitalinput.go @@ -1,4 +1,4 @@ -package rev1 +package common import ( "time" @@ -7,17 +7,17 @@ import ( "github.com/awonak/EuroPiGo/hardware/hal" ) -// digitalinput is a struct for handling reading of the digital input. -type digitalinput struct { +// 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{} + _ hal.DigitalInput = (*Digitalinput)(nil) // silence linter - _ = newDigitalInput + _ = NewDigitalInput ) type DigitalReaderProvider interface { @@ -25,31 +25,34 @@ type DigitalReaderProvider interface { SetHandler(changes hal.ChangeFlags, handler func()) } -// newDigitalInput creates a new digital input struct. -func newDigitalInput(dr DigitalReaderProvider) *digitalinput { - return &digitalinput{ +// 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 { +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 { +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)) { +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)) { +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) @@ -59,7 +62,7 @@ func (d *digitalinput) HandlerEx(changes hal.ChangeFlags, handler func(value boo } // 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) { +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 diff --git a/hardware/rev1/displayoutput.go b/hardware/common/displayoutput.go similarity index 58% rename from hardware/rev1/displayoutput.go rename to hardware/common/displayoutput.go index 3d84dee..dc32455 100644 --- a/hardware/rev1/displayoutput.go +++ b/hardware/common/displayoutput.go @@ -1,4 +1,4 @@ -package rev1 +package common import ( "image/color" @@ -6,16 +6,16 @@ import ( "github.com/awonak/EuroPiGo/hardware/hal" ) -// displayoutput is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED. -type displayoutput struct { +// 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{} + _ hal.DisplayOutput = (*DisplayOutput)(nil) // silence linter - _ = newDisplayOutput + _ = NewDisplayOutput ) type DisplayProvider interface { @@ -25,35 +25,38 @@ type DisplayProvider interface { Display() error } -// newDisplayOutput returns a new Display struct. -func newDisplayOutput(dp DisplayProvider) hal.DisplayOutput { - return &displayoutput{ +// 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 { +func (d *DisplayOutput) Configure(config hal.DisplayOutputConfig) error { return nil } // ClearBuffer clears the internal display buffer for the device -func (d *displayoutput) ClearBuffer() { +func (d *DisplayOutput) ClearBuffer() { d.dp.ClearBuffer() } // Size returns the display resolution for the device -func (d *displayoutput) Size() (x, y int16) { +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) { +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 { +func (d *DisplayOutput) Display() error { return d.dp.Display() } diff --git a/hardware/rev1/randomgenerator.go b/hardware/common/randomgenerator.go similarity index 60% rename from hardware/rev1/randomgenerator.go rename to hardware/common/randomgenerator.go index cc01f60..d452f4b 100644 --- a/hardware/rev1/randomgenerator.go +++ b/hardware/common/randomgenerator.go @@ -1,22 +1,22 @@ -package rev1 +package common import ( "github.com/awonak/EuroPiGo/hardware/hal" ) -type randomGenerator struct { +type RandomGenerator struct { rnd RNDProvider } var ( // static check - _ hal.RandomGenerator = &randomGenerator{} + _ hal.RandomGenerator = (*RandomGenerator)(nil) // silence linter - _ = newRandomGenerator + _ = NewRandomGenerator ) -func newRandomGenerator(rnd RNDProvider) hal.RandomGenerator { - return &randomGenerator{ +func NewRandomGenerator(rnd RNDProvider) *RandomGenerator { + return &RandomGenerator{ rnd: rnd, } } @@ -26,7 +26,7 @@ type RNDProvider interface { } // Configure updates the device with various configuration parameters -func (r *randomGenerator) Configure(config hal.RandomGeneratorConfig) error { +func (r *RandomGenerator) Configure(config hal.RandomGeneratorConfig) error { if r.rnd != nil { if err := r.rnd.Configure(config); err != nil { return err 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/hardware.go b/hardware/hal/hardware.go index 22f2bd5..ca91bfa 100644 --- a/hardware/hal/hardware.go +++ b/hardware/hal/hardware.go @@ -20,16 +20,20 @@ const ( 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 - HardwareIdCV1Output = HardwareIdVoltage1Output - HardwareIdCV2Output = HardwareIdVoltage2Output - HardwareIdCV3Output = HardwareIdVoltage3Output - HardwareIdCV4Output = HardwareIdVoltage4Output - HardwareIdCV5Output = HardwareIdVoltage5Output - HardwareIdCV6Output = HardwareIdVoltage6Output ) + +// Hardware is the collection of component wrappers used to interact with the module. +type Hardware interface { + Revision() Revision + Random() RandomGenerator + Button(idx int) ButtonInput + Knob(idx int) KnobInput +} diff --git a/hardware/hal/voltageoutput.go b/hardware/hal/voltageoutput.go index ac3f89e..8cf11c3 100644 --- a/hardware/hal/voltageoutput.go +++ b/hardware/hal/voltageoutput.go @@ -18,6 +18,7 @@ type VoltageOutput interface { } type VoltageOutputConfig struct { - Period time.Duration - Calibration envelope.Map[float32, uint16] + Period time.Duration + PerformWavefold bool + Calibration envelope.Map[float32, uint16] } diff --git a/hardware/platform.go b/hardware/platform.go index b6b17c9..bd4c6db 100644 --- a/hardware/platform.go +++ b/hardware/platform.go @@ -12,6 +12,9 @@ import ( // 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 rev1.GetHardware[T](id) + case hal.Revision1: return rev1.GetHardware[T](id) 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..476696a --- /dev/null +++ b/hardware/rev0/analoginput.go @@ -0,0 +1,34 @@ +package rev0 + +import ( + "github.com/awonak/EuroPiGo/experimental/envelope" + "github.com/awonak/EuroPiGo/hardware/hal" +) + +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 ( + aiInitialConfig = hal.AnalogInputConfig{ + Samples: DefaultSamples, + Calibration: envelope.NewMap32([]envelope.MapEntry[uint16, float32]{ + { + Input: DefaultCalibratedMinAI, + Output: MinInputVoltage, + }, + { + Input: DefaultCalibratedMaxAI, + Output: MaxInputVoltage, + }, + }), + } +) 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..1d58dc8 --- /dev/null +++ b/hardware/rev0/platform.go @@ -0,0 +1,162 @@ +package rev0 + +import ( + "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 { + // 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) Revision() hal.Revision { + return hal.Revision0 +} + +func (e *EuroPiPrototype) Random() hal.RandomGenerator { + return e.RND +} + +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{ + 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..978c2d1 --- /dev/null +++ b/hardware/rev0/voltageoutput.go @@ -0,0 +1,40 @@ +package rev0 + +import ( + "time" + + "github.com/awonak/EuroPiGo/experimental/envelope" + "github.com/awonak/EuroPiGo/hardware/hal" +) + +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. +var DefaultPWMPeriod time.Duration = time.Nanosecond * 500 + +var ( + cvInitialConfig = hal.VoltageOutputConfig{ + Period: DefaultPWMPeriod, + PerformWavefold: true, + Calibration: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ + { + Input: MinOutputVoltage, + Output: CalibratedTop, + }, + { + Input: MaxOutputVoltage, + Output: CalibratedOffset, + }, + }), + } +) diff --git a/hardware/rev1/README.md b/hardware/rev1/README.md index 73582d6..44a8a55 100644 --- a/hardware/rev1/README.md +++ b/hardware/rev1/README.md @@ -6,7 +6,6 @@ This package is used for the [Original EuroPi hardware](https://github.com/Allen | Name | Interface | HardwareId | HardwareId Alias | |----|----|----|----| -| `RevisionMarker` | `hal.RevisionMarker` | `HardwareIdRevisionMarker` | | | `InputDigital1` | `hal.DigitalInput` | `HardwareIdDigital1Input` | | | `InputAnalog1` | `hal.AnalogInput` | `HardwareIdAnalog1Input` | `HardwareIdAnalogue1Input` | | `OutputDisplay1` | `hal.DisplayOutput` | `HardwareIdDisplay1Output` | | diff --git a/hardware/rev1/analoginput.go b/hardware/rev1/analoginput.go index aeba5da..390e4ae 100644 --- a/hardware/rev1/analoginput.go +++ b/hardware/rev1/analoginput.go @@ -1,12 +1,8 @@ package rev1 import ( - "errors" - - "github.com/awonak/EuroPiGo/clamp" "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/units" ) const ( @@ -21,31 +17,10 @@ const ( MinInputVoltage = 0.0 ) -// 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 envelope.Map[uint16, float32] -} - var ( - // static check - _ hal.AnalogInput = &analoginput{} - // silence linter - _ = newAnalogInput -) - -type ADCProvider interface { - Get(samples int) uint16 -} - -// newAnalogInput creates a new Analog Input -func newAnalogInput(adc ADCProvider) *analoginput { - return &analoginput{ - adc: adc, - samples: DefaultSamples, - cal: envelope.NewMap32([]envelope.MapEntry[uint16, float32]{ + aiInitialConfig = hal.AnalogInputConfig{ + Samples: DefaultSamples, + Calibration: envelope.NewMap32([]envelope.MapEntry[uint16, float32]{ { Input: DefaultCalibratedMinAI, Output: MinInputVoltage, @@ -56,66 +31,4 @@ func newAnalogInput(adc ADCProvider) *analoginput { }, }), } -} - -// 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() / MaxInputVoltage -} - -// 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/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 index d0709cc..36dff85 100644 --- a/hardware/rev1/platform.go +++ b/hardware/rev1/platform.go @@ -1,61 +1,116 @@ package rev1 import ( + "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" ) -// These will be configured during `init()` from platform-specific files. +// Pi will be configured during `init()` from platform-specific files. // See `hardware/pico/pico.go` and `hardware/nonpico/nonpico.go` for more information. -var ( - InputDigital1 hal.DigitalInput - InputAnalog1 hal.AnalogInput - OutputDisplay1 hal.DisplayOutput - InputButton1 hal.ButtonInput - InputButton2 hal.ButtonInput - InputKnob1 hal.KnobInput - InputKnob2 hal.KnobInput - OutputVoltage1 hal.VoltageOutput - OutputVoltage2 hal.VoltageOutput - OutputVoltage3 hal.VoltageOutput - OutputVoltage4 hal.VoltageOutput - OutputVoltage5 hal.VoltageOutput - OutputVoltage6 hal.VoltageOutput - DeviceRandomGenerator1 hal.RandomGenerator -) +var Pi *EuroPi + +type EuroPi struct { + // 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) Revision() hal.Revision { + return hal.Revision1 +} + +func (e *EuroPi) Random() hal.RandomGenerator { + return e.RND +} + +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 hal.HardwareIdDigital1Input: - t, _ = InputDigital1.(T) - case hal.HardwareIdAnalog1Input: - t, _ = InputAnalog1.(T) - case hal.HardwareIdDisplay1Output: - t, _ = OutputDisplay1.(T) - case hal.HardwareIdButton1Input: - t, _ = InputButton1.(T) - case hal.HardwareIdButton2Input: - t, _ = InputButton2.(T) - case hal.HardwareIdKnob1Input: - t, _ = InputKnob1.(T) - case hal.HardwareIdKnob2Input: - t, _ = InputKnob2.(T) - case hal.HardwareIdVoltage1Output: - t, _ = OutputVoltage1.(T) - case hal.HardwareIdVoltage2Output: - t, _ = OutputVoltage2.(T) - case hal.HardwareIdVoltage3Output: - t, _ = OutputVoltage3.(T) - case hal.HardwareIdVoltage4Output: - t, _ = OutputVoltage4.(T) - case hal.HardwareIdVoltage5Output: - t, _ = OutputVoltage5.(T) - case hal.HardwareIdVoltage6Output: - t, _ = OutputVoltage6.(T) - case hal.HardwareIdRandom1Generator: - t, _ = DeviceRandomGenerator1.(T) + 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 @@ -65,38 +120,40 @@ func GetHardware[T any](hw hal.HardwareId) T { // // This is only to be called by the automatic platform initialization functions func Initialize(params InitializationParameters) { - InputDigital1 = newDigitalInput(params.InputDigital1) - InputAnalog1 = newAnalogInput(params.InputAnalog1) - OutputDisplay1 = newDisplayOutput(params.OutputDisplay1) - InputButton1 = newDigitalInput(params.InputButton1) - InputButton2 = newDigitalInput(params.InputButton2) - InputKnob1 = newAnalogInput(params.InputKnob1) - InputKnob2 = newAnalogInput(params.InputKnob2) - OutputVoltage1 = newVoltageOuput(params.OutputVoltage1) - OutputVoltage2 = newVoltageOuput(params.OutputVoltage2) - OutputVoltage3 = newVoltageOuput(params.OutputVoltage3) - OutputVoltage4 = newVoltageOuput(params.OutputVoltage4) - OutputVoltage5 = newVoltageOuput(params.OutputVoltage5) - OutputVoltage6 = newVoltageOuput(params.OutputVoltage6) - DeviceRandomGenerator1 = newRandomGenerator(params.DeviceRandomGenerator1) + Pi = &EuroPi{ + 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 DigitalReaderProvider - InputAnalog1 ADCProvider - OutputDisplay1 DisplayProvider - InputButton1 DigitalReaderProvider - InputButton2 DigitalReaderProvider - InputKnob1 ADCProvider - InputKnob2 ADCProvider - OutputVoltage1 PWMProvider - OutputVoltage2 PWMProvider - OutputVoltage3 PWMProvider - OutputVoltage4 PWMProvider - OutputVoltage5 PWMProvider - OutputVoltage6 PWMProvider - DeviceRandomGenerator1 RNDProvider + 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 index 07758c7..2548165 100644 --- a/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -1,12 +1,10 @@ package rev1 import ( - "fmt" "time" "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/units" ) const ( @@ -22,35 +20,12 @@ const ( // 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 time.Duration = time.Nanosecond * 500 - -// voltageoutput is struct for interacting with the CV/VOct voltage output jacks. -type voltageoutput struct { - pwm PWMProvider -} +var DefaultPWMPeriod time.Duration = time.Nanosecond * 500 var ( - // static check - _ hal.VoltageOutput = &voltageoutput{} - // 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) hal.VoltageOutput { - o := &voltageoutput{ - pwm: pwm, - } - err := o.Configure(hal.VoltageOutputConfig{ - Period: defaultPeriod, + cvInitialConfig = hal.VoltageOutputConfig{ + Period: DefaultPWMPeriod, + PerformWavefold: true, Calibration: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ { Input: MinOutputVoltage, @@ -61,54 +36,5 @@ func newVoltageOuput(pwm PWMProvider) hal.VoltageOutput { Output: CalibratedOffset, }, }), - }) - 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/internal/nonpico/rev1/adc.go b/internal/nonpico/common/adc.go similarity index 73% rename from internal/nonpico/rev1/adc.go rename to internal/nonpico/common/adc.go index 44b4a56..1ffd2f7 100644 --- a/internal/nonpico/rev1/adc.go +++ b/internal/nonpico/common/adc.go @@ -1,14 +1,14 @@ //go:build !pico // +build !pico -package rev1 +package common import ( "fmt" "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" ) type nonPicoAdc struct { @@ -17,7 +17,12 @@ type nonPicoAdc struct { value uint16 } -func newNonPicoAdc(bus event.Bus, id hal.HardwareId) rev1.ADCProvider { +var ( + // static check + _ common.ADCProvider = (*nonPicoAdc)(nil) +) + +func NewNonPicoAdc(bus event.Bus, id hal.HardwareId) *nonPicoAdc { adc := &nonPicoAdc{ bus: bus, id: id, diff --git a/internal/nonpico/rev1/digitalreader.go b/internal/nonpico/common/digitalreader.go similarity index 66% rename from internal/nonpico/rev1/digitalreader.go rename to internal/nonpico/common/digitalreader.go index 44686e5..ab1f6a2 100644 --- a/internal/nonpico/rev1/digitalreader.go +++ b/internal/nonpico/common/digitalreader.go @@ -1,14 +1,14 @@ //go:build !pico // +build !pico -package rev1 +package common import ( "fmt" "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" ) type nonPicoDigitalReader struct { @@ -17,13 +17,19 @@ type nonPicoDigitalReader struct { value bool } -func newNonPicoDigitalReader(bus event.Bus, id hal.HardwareId) rev1.DigitalReaderProvider { +var ( + // static check + _ common.DigitalReaderProvider = (*nonPicoDigitalReader)(nil) +) + +func NewNonPicoDigitalReader(bus event.Bus, id hal.HardwareId) *nonPicoDigitalReader { dr := &nonPicoDigitalReader{ - bus: bus, - id: id, + bus: bus, + 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 + dr.value = !msg.Value }) return dr } 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/rev1/displayoutput.go b/internal/nonpico/common/displayoutput.go similarity index 82% rename from internal/nonpico/rev1/displayoutput.go rename to internal/nonpico/common/displayoutput.go index c7a3002..9ccfcbd 100644 --- a/internal/nonpico/rev1/displayoutput.go +++ b/internal/nonpico/common/displayoutput.go @@ -1,15 +1,15 @@ //go:build !pico // +build !pico -package rev1 +package common import ( "fmt" "image/color" "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" ) const ( @@ -24,7 +24,12 @@ type nonPicoDisplayOutput struct { height int16 } -func newNonPicoDisplayOutput(bus event.Bus, id hal.HardwareId) rev1.DisplayProvider { +var ( + // static check + _ common.DisplayProvider = (*nonPicoDisplayOutput)(nil) +) + +func NewNonPicoDisplayOutput(bus event.Bus, id hal.HardwareId) *nonPicoDisplayOutput { dp := &nonPicoDisplayOutput{ bus: bus, id: id, diff --git a/internal/nonpico/rev1/messages.go b/internal/nonpico/common/messages.go similarity index 94% rename from internal/nonpico/rev1/messages.go rename to internal/nonpico/common/messages.go index 2f777f5..5780de4 100644 --- a/internal/nonpico/rev1/messages.go +++ b/internal/nonpico/common/messages.go @@ -1,4 +1,4 @@ -package rev1 +package common import "github.com/awonak/EuroPiGo/hardware/hal" @@ -19,7 +19,8 @@ type HwMessageInterrupt struct { // HwMessagePwmValue represents a pulse width modulator value update type HwMessagePwmValue struct { - Value uint16 + Value uint16 + Voltage float32 } // HwMessageDisplay represents a display update. diff --git a/internal/nonpico/rev1/pwm.go b/internal/nonpico/common/pwm.go similarity index 64% rename from internal/nonpico/rev1/pwm.go rename to internal/nonpico/common/pwm.go index 766d4ab..1d44eb7 100644 --- a/internal/nonpico/rev1/pwm.go +++ b/internal/nonpico/common/pwm.go @@ -1,15 +1,15 @@ //go:build !pico // +build !pico -package rev1 +package common import ( "fmt" "github.com/awonak/EuroPiGo/event" "github.com/awonak/EuroPiGo/experimental/envelope" + "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" ) type nonPicoPwm struct { @@ -19,20 +19,16 @@ type nonPicoPwm struct { v float32 } -func newNonPicoPwm(bus event.Bus, id hal.HardwareId) rev1.PWMProvider { +var ( + // static check + _ common.PWMProvider = (*nonPicoPwm)(nil) +) + +func NewNonPicoPwm(bus event.Bus, id hal.HardwareId, cal envelope.Map[float32, uint16]) *nonPicoPwm { p := &nonPicoPwm{ bus: bus, id: id, - cal: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ - { - Input: rev1.MinOutputVoltage, - Output: rev1.CalibratedTop, - }, - { - Input: rev1.MaxOutputVoltage, - Output: rev1.CalibratedOffset, - }, - }), + cal: cal, } return p } @@ -42,10 +38,11 @@ func (p *nonPicoPwm) Configure(config hal.VoltageOutputConfig) error { } func (p *nonPicoPwm) Set(v float32) { - volts := p.cal.Remap(v) - p.v = v + pulseWidth := p.cal.Remap(v) + p.v = p.cal.Unmap(pulseWidth) p.bus.Post(fmt.Sprintf("hw_pwm_%d", p.id), HwMessagePwmValue{ - Value: uint16(volts), + Value: pulseWidth, + Voltage: p.v, }) } diff --git a/internal/nonpico/nonpico.go b/internal/nonpico/nonpico.go index 90eb4ba..bf8b0d5 100644 --- a/internal/nonpico/nonpico.go +++ b/internal/nonpico/nonpico.go @@ -6,9 +6,14 @@ 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() } @@ -20,6 +25,8 @@ func initRevision2() { func init() { hardware.OnRevisionDetected <- func(revision hal.Revision) { switch revision { + case hal.Revision0: + initRevision0() case hal.Revision1: initRevision1() case hal.Revision2: diff --git a/internal/nonpico/rev0.go b/internal/nonpico/rev0.go new file mode 100644 index 0000000..357e53d --- /dev/null +++ b/internal/nonpico/rev0.go @@ -0,0 +1,14 @@ +//go:build !pico && (revision0 || europiproto || europiprototype) +// +build !pico +// +build revision0 europiproto europiprototype + +package nonpico + +import ( + "github.com/awonak/EuroPiGo/hardware" + "github.com/awonak/EuroPiGo/hardware/hal" +) + +func init() { + hardware.SetDetectedRevision(hal.Revision0) +} 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..3aa52bf --- /dev/null +++ b/internal/nonpico/rev0/listeners.go @@ -0,0 +1,77 @@ +//go:build !pico +// +build !pico + +package rev0 + +import ( + "fmt" + "sync" + + "github.com/awonak/EuroPiGo/event" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev0" + "github.com/awonak/EuroPiGo/internal/nonpico/common" + "github.com/awonak/EuroPiGo/lerp" +) + +var ( + bus = event.NewBus() +) + +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 { + fn := func(hid hal.HardwareId) func(common.HwMessagePwmValue) { + return func(msg common.HwMessagePwmValue) { + cb(hid, msg.Voltage) + } + }(id) + event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", id), fn) + } +} + +var ( + states sync.Map +) + +func setDigitalInput(id hal.HardwareId, value bool) { + prevState, _ := states.Load(id) + + states.Store(id, value) + bus.Post(fmt.Sprintf("hw_value_%d", id), common.HwMessageDigitalValue{ + Value: value, + }) + + if prevState != value { + if value { + // rising + bus.Post(fmt.Sprintf("hw_interrupt_%d", id), common.HwMessageInterrupt{ + Change: hal.ChangeRising, + }) + } else { + // falling + bus.Post(fmt.Sprintf("hw_interrupt_%d", id), common.HwMessageInterrupt{ + Change: hal.ChangeFalling, + }) + } + } +} + +var ( + aiLerp = lerp.NewLerp32[uint16](rev0.DefaultCalibratedMinAI, rev0.DefaultCalibratedMaxAI) +) + +func setAnalogInput(id hal.HardwareId, voltage float32) { + bus.Post(fmt.Sprintf("hw_value_%d", id), common.HwMessageADCValue{ + Value: aiLerp.Lerp(voltage), + }) +} diff --git a/internal/nonpico/rev0/platform.go b/internal/nonpico/rev0/platform.go new file mode 100644 index 0000000..328ff2c --- /dev/null +++ b/internal/nonpico/rev0/platform.go @@ -0,0 +1,35 @@ +package rev0 + +import ( + "github.com/awonak/EuroPiGo/experimental/envelope" + "github.com/awonak/EuroPiGo/hardware/rev0" + "github.com/awonak/EuroPiGo/internal/nonpico/common" +) + +func DoInit() { + cvCalMap := envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ + { + Input: rev0.MinOutputVoltage, + Output: rev0.CalibratedTop, + }, + { + Input: rev0.MaxOutputVoltage, + Output: rev0.CalibratedOffset, + }, + }) + rev0.Initialize(rev0.InitializationParameters{ + InputButton1: common.NewNonPicoDigitalReader(bus, rev0.HardwareIdButton1Input), + InputButton2: common.NewNonPicoDigitalReader(bus, rev0.HardwareIdButton2Input), + InputKnob1: common.NewNonPicoAdc(bus, rev0.HardwareIdKnob1Input), + InputKnob2: common.NewNonPicoAdc(bus, rev0.HardwareIdKnob2Input), + OutputAnalog1: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog1Output, cvCalMap), + OutputAnalog2: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog2Output, cvCalMap), + OutputAnalog3: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog3Output, cvCalMap), + OutputAnalog4: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog4Output, cvCalMap), + OutputDigital1: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital1Output, cvCalMap), + OutputDigital2: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital2Output, cvCalMap), + OutputDigital3: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital3Output, cvCalMap), + OutputDigital4: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital4Output, cvCalMap), + 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..bda381f --- /dev/null +++ b/internal/nonpico/rev0/wsactivation.go @@ -0,0 +1,126 @@ +//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) + + 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 index 4432cf7..e652fce 100644 --- a/internal/nonpico/rev1/api.go +++ b/internal/nonpico/rev1/api.go @@ -5,6 +5,7 @@ package rev1 import ( "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/internal/nonpico/common" ) type voltageOutputMsg struct { @@ -15,10 +16,10 @@ type voltageOutputMsg struct { // displayMode = displayModeSeparate (0) type displayOutputMsg struct { - Kind string `json:"kind"` - HardwareId hal.HardwareId `json:"hardwareId"` - Op HwDisplayOp `json:"op"` - Params []int16 `json:"params"` + Kind string `json:"kind"` + HardwareId hal.HardwareId `json:"hardwareId"` + Op common.HwDisplayOp `json:"op"` + Params []int16 `json:"params"` } // displayMode = displayModeCombined (1) diff --git a/internal/nonpico/rev1/displaymode.go b/internal/nonpico/rev1/displaymode.go deleted file mode 100644 index bac3585..0000000 --- a/internal/nonpico/rev1/displaymode.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !pico -// +build !pico - -package rev1 - -type displayMode int - -const ( - displayModeSeparate = displayMode(iota) - displayModeCombined -) diff --git a/internal/nonpico/rev1/listeners.go b/internal/nonpico/rev1/listeners.go index da1393e..72a0e04 100644 --- a/internal/nonpico/rev1/listeners.go +++ b/internal/nonpico/rev1/listeners.go @@ -5,36 +5,34 @@ package rev1 import ( "fmt" - "math" "sync" "github.com/awonak/EuroPiGo/event" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev1" + "github.com/awonak/EuroPiGo/internal/nonpico/common" "github.com/awonak/EuroPiGo/lerp" ) var ( - bus = event.NewBus() - voLerp = lerp.NewLerp32[uint16](0, math.MaxUint16) + bus = event.NewBus() ) func setupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) { for id := hal.HardwareIdVoltage1Output; id <= hal.HardwareIdVoltage6Output; id++ { - fn := func(hid hal.HardwareId) func(HwMessagePwmValue) { - return func(msg HwMessagePwmValue) { - v := voLerp.ClampedInverseLerp(msg.Value) * rev1.MaxOutputVoltage - cb(hid, v) + fn := func(hid hal.HardwareId) func(common.HwMessagePwmValue) { + return func(msg common.HwMessagePwmValue) { + cb(hid, msg.Voltage) } }(id) event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", id), fn) } } -func setupDisplayOutputListener(cb func(id hal.HardwareId, op HwDisplayOp, params []int16)) { +func setupDisplayOutputListener(cb func(id hal.HardwareId, op common.HwDisplayOp, params []int16)) { bus := bus id := hal.HardwareIdDisplay1Output - event.Subscribe(bus, fmt.Sprintf("hw_display_%d", id), func(msg HwMessageDisplay) { + event.Subscribe(bus, fmt.Sprintf("hw_display_%d", id), func(msg common.HwMessageDisplay) { cb(id, msg.Op, msg.Operands) }) @@ -48,19 +46,19 @@ func setDigitalInput(id hal.HardwareId, value bool) { prevState, _ := states.Load(id) states.Store(id, value) - bus.Post(fmt.Sprintf("hw_value_%d", id), HwMessageDigitalValue{ + bus.Post(fmt.Sprintf("hw_value_%d", id), common.HwMessageDigitalValue{ Value: value, }) if prevState != value { if value { // rising - bus.Post(fmt.Sprintf("hw_interrupt_%d", id), HwMessageInterrupt{ + bus.Post(fmt.Sprintf("hw_interrupt_%d", id), common.HwMessageInterrupt{ Change: hal.ChangeRising, }) } else { // falling - bus.Post(fmt.Sprintf("hw_interrupt_%d", id), HwMessageInterrupt{ + bus.Post(fmt.Sprintf("hw_interrupt_%d", id), common.HwMessageInterrupt{ Change: hal.ChangeFalling, }) } @@ -72,7 +70,7 @@ var ( ) func setAnalogInput(id hal.HardwareId, voltage float32) { - bus.Post(fmt.Sprintf("hw_value_%d", id), HwMessageADCValue{ + bus.Post(fmt.Sprintf("hw_value_%d", id), common.HwMessageADCValue{ Value: aiLerp.Lerp(voltage), }) } diff --git a/internal/nonpico/rev1/platform.go b/internal/nonpico/rev1/platform.go index 1f18e1f..ff8a947 100644 --- a/internal/nonpico/rev1/platform.go +++ b/internal/nonpico/rev1/platform.go @@ -1,25 +1,36 @@ package rev1 import ( - "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/rev1" + "github.com/awonak/EuroPiGo/internal/nonpico/common" ) func DoInit() { + cvCalMap := envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ + { + Input: rev1.MinOutputVoltage, + Output: rev1.CalibratedTop, + }, + { + Input: rev1.MaxOutputVoltage, + Output: rev1.CalibratedOffset, + }, + }) rev1.Initialize(rev1.InitializationParameters{ - InputDigital1: newNonPicoDigitalReader(bus, hal.HardwareIdDigital1Input), - InputAnalog1: newNonPicoAdc(bus, hal.HardwareIdAnalog1Input), - OutputDisplay1: newNonPicoDisplayOutput(bus, hal.HardwareIdDisplay1Output), - InputButton1: newNonPicoDigitalReader(bus, hal.HardwareIdButton1Input), - InputButton2: newNonPicoDigitalReader(bus, hal.HardwareIdButton2Input), - InputKnob1: newNonPicoAdc(bus, hal.HardwareIdKnob1Input), - InputKnob2: newNonPicoAdc(bus, hal.HardwareIdKnob2Input), - OutputVoltage1: newNonPicoPwm(bus, hal.HardwareIdVoltage1Output), - OutputVoltage2: newNonPicoPwm(bus, hal.HardwareIdVoltage2Output), - OutputVoltage3: newNonPicoPwm(bus, hal.HardwareIdVoltage3Output), - OutputVoltage4: newNonPicoPwm(bus, hal.HardwareIdVoltage4Output), - OutputVoltage5: newNonPicoPwm(bus, hal.HardwareIdVoltage5Output), - OutputVoltage6: newNonPicoPwm(bus, hal.HardwareIdVoltage6Output), + InputDigital1: common.NewNonPicoDigitalReader(bus, rev1.HardwareIdDigital1Input), + InputAnalog1: common.NewNonPicoAdc(bus, rev1.HardwareIdAnalog1Input), + OutputDisplay1: common.NewNonPicoDisplayOutput(bus, rev1.HardwareIdDisplay1Output), + InputButton1: common.NewNonPicoDigitalReader(bus, rev1.HardwareIdButton1Input), + InputButton2: common.NewNonPicoDigitalReader(bus, rev1.HardwareIdButton2Input), + InputKnob1: common.NewNonPicoAdc(bus, rev1.HardwareIdKnob1Input), + InputKnob2: common.NewNonPicoAdc(bus, rev1.HardwareIdKnob2Input), + OutputVoltage1: common.NewNonPicoPwm(bus, rev1.HardwareIdCV1Output, cvCalMap), + OutputVoltage2: common.NewNonPicoPwm(bus, rev1.HardwareIdCV2Output, cvCalMap), + OutputVoltage3: common.NewNonPicoPwm(bus, rev1.HardwareIdCV3Output, cvCalMap), + OutputVoltage4: common.NewNonPicoPwm(bus, rev1.HardwareIdCV4Output, cvCalMap), + OutputVoltage5: common.NewNonPicoPwm(bus, rev1.HardwareIdCV5Output, cvCalMap), + OutputVoltage6: common.NewNonPicoPwm(bus, rev1.HardwareIdCV6Output, cvCalMap), DeviceRandomGenerator1: nil, }) } diff --git a/internal/nonpico/rev1/wsactivation.go b/internal/nonpico/rev1/wsactivation.go index 7074e72..969a6e3 100644 --- a/internal/nonpico/rev1/wsactivation.go +++ b/internal/nonpico/rev1/wsactivation.go @@ -14,19 +14,20 @@ import ( "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 displayMode + displayMode common.DisplayMode } -func ActivateWebSocket() *WSActivation { +func ActivateWebSocket(ctx context.Context) *WSActivation { a := &WSActivation{} - a.Start(context.Background()) + a.Start(ctx) return a } @@ -66,7 +67,7 @@ func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() dm, _ := strconv.Atoi(q.Get("displayMode")) - a.displayMode = displayMode(dm) + a.displayMode = common.DisplayMode(dm) sock, err := ws.Upgrade(w, r) if err != nil { @@ -92,15 +93,15 @@ func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) { Height: displayHeight, Data: make([]byte, displayWidth*displayHeight*4), } - setupDisplayOutputListener(func(id hal.HardwareId, op HwDisplayOp, params []int16) { + setupDisplayOutputListener(func(id hal.HardwareId, op common.HwDisplayOp, params []int16) { switch a.displayMode { - case displayModeCombined: + case common.DisplayModeCombined: switch op { - case HwDisplayOpClearBuffer: + case common.HwDisplayOpClearBuffer: for i := range displayScreenOutputMsg.Data { displayScreenOutputMsg.Data[i] = 0 } - case HwDisplayOpSetPixel: + case common.HwDisplayOpSetPixel: y, x := int(params[1]), int(params[0]) if y < 0 || y >= displayHeight || x < 0 || x >= displayWidth { break @@ -110,7 +111,7 @@ func (a *WSActivation) apiHandler(w http.ResponseWriter, r *http.Request) { displayScreenOutputMsg.Data[pos+1] = byte(params[3]) displayScreenOutputMsg.Data[pos+2] = byte(params[4]) displayScreenOutputMsg.Data[pos+3] = byte(params[5]) - case HwDisplayOpDisplay: + case common.HwDisplayOpDisplay: _ = sock.WriteJSON(displayScreenOutputMsg) default: } diff --git a/internal/nonpico/wsactivator.go b/internal/nonpico/wsactivator.go index 66966eb..d6e415b 100644 --- a/internal/nonpico/wsactivator.go +++ b/internal/nonpico/wsactivator.go @@ -4,7 +4,10 @@ package nonpico import ( + "context" + "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/internal/nonpico/rev0" "github.com/awonak/EuroPiGo/internal/nonpico/rev1" ) @@ -12,11 +15,12 @@ type WSActivation interface { Shutdown() error } -func ActivateWebSocket(revision hal.Revision) WSActivation { +func ActivateWebSocket(ctx context.Context, revision hal.Revision) WSActivation { switch revision { + case hal.Revision0: + return rev0.ActivateWebSocket(ctx) case hal.Revision1: - return rev1.ActivateWebSocket() - + return rev1.ActivateWebSocket(ctx) default: return nil } diff --git a/internal/pico/display.go b/internal/pico/display.go index c2b7418..617e701 100644 --- a/internal/pico/display.go +++ b/internal/pico/display.go @@ -7,7 +7,6 @@ import ( "image/color" "machine" - "github.com/awonak/EuroPiGo/hardware/rev1" "tinygo.org/x/drivers/ssd1306" ) @@ -22,7 +21,7 @@ type picoDisplayOutput struct { dev ssd1306.Device } -func newPicoDisplayOutput(channel *machine.I2C, sdaPin, sclPin machine.Pin) rev1.DisplayProvider { +func newPicoDisplayOutput(channel *machine.I2C, sdaPin, sclPin machine.Pin) *picoDisplayOutput { channel.Configure(machine.I2CConfig{ Frequency: oledFreq, SDA: sdaPin, diff --git a/internal/pico/pico.go b/internal/pico/pico.go index 95a8a58..a406610 100644 --- a/internal/pico/pico.go +++ b/internal/pico/pico.go @@ -11,6 +11,26 @@ import ( "github.com/awonak/EuroPiGo/hardware/rev1" ) +// EuroPi Prototype +// Rev0 leverages Rev1 layout and functions +func initRevision0() { + rev1.Initialize(rev1.InitializationParameters{ + InputButton1: newPicoDigitalReader(machine.GPIO15), + InputButton2: newPicoDigitalReader(machine.GPIO18), + InputKnob1: newPicoAdc(machine.ADC2), + InputKnob2: newPicoAdc(machine.ADC1), + OutputVoltage1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeDigitalRevision0), + OutputVoltage2: newPicoPwm(machine.PWM3, machine.GPIO22, picoPwmModeDigitalRevision0), + OutputVoltage3: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeDigitalRevision0), + OutputVoltage4: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeDigitalRevision0), + OutputVoltage5: newPicoPwm(machine.PWM7, machine.GPIO14, picoPwmModeAnalogRevision0), + OutputVoltage6: newPicoPwm(machine.PWM5, machine.GPIO11, picoPwmModeAnalogRevision0), + OutputVoltage7: newPicoPwm(machine.PWM5, machine.GPIO10, picoPwmModeAnalogRevision0), + OutputVoltage8: newPicoPwm(machine.PWM3, machine.GPIO7, picoPwmModeAnalogRevision0), + DeviceRandomGenerator1: &picoRnd{}, + }) +} + // EuroPi (original) func initRevision1() { rev1.Initialize(rev1.InitializationParameters{ @@ -21,12 +41,12 @@ func initRevision1() { InputButton2: newPicoDigitalReader(machine.GPIO5), InputKnob1: newPicoAdc(machine.ADC1), InputKnob2: newPicoAdc(machine.ADC2), - 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), + OutputVoltage1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeAnalogRevision1), + OutputVoltage2: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeAnalogRevision1), + OutputVoltage3: newPicoPwm(machine.PWM0, machine.GPIO16, picoPwmModeAnalogRevision1), + OutputVoltage4: newPicoPwm(machine.PWM0, machine.GPIO17, picoPwmModeAnalogRevision1), + OutputVoltage5: newPicoPwm(machine.PWM1, machine.GPIO18, picoPwmModeAnalogRevision1), + OutputVoltage6: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeAnalogRevision1), DeviceRandomGenerator1: &picoRnd{}, }) } @@ -39,6 +59,8 @@ func initRevision2() { func init() { hardware.OnRevisionDetected <- func(revision hal.Revision) { switch revision { + case hal.Revision0: + initRevision0() case hal.Revision1: initRevision1() case hal.Revision2: diff --git a/internal/pico/pwm.go b/internal/pico/pwm.go index 3db55b2..eaeb96e 100644 --- a/internal/pico/pwm.go +++ b/internal/pico/pwm.go @@ -9,18 +9,21 @@ import ( "math" "runtime/interrupt" "runtime/volatile" + "time" "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" - "github.com/awonak/EuroPiGo/hardware/rev1" + "github.com/awonak/EuroPiGo/hardware/rev0" ) type picoPwm struct { - pwm pwmGroup - pin machine.Pin - ch uint8 - v uint32 - cal envelope.Map[float32, uint16] + pwm pwmGroup + pin machine.Pin + ch uint8 + v uint32 + period time.Duration + wavefold bool + cal envelope.Map[float32, uint16] } // pwmGroup is an interface for interacting with a machine.pwmGroup @@ -34,18 +37,27 @@ type pwmGroup interface { SetPeriod(period uint64) error } -func newPicoPwm(pwm pwmGroup, pin machine.Pin) rev1.PWMProvider { +type picoPwmMode int + +const ( + picoPwmModeAnalogRevision0 = picoPwmMode(iota) + picoPwmModeDigitalRevision0 + picoPwmModeAnalogRevision1 +) + +func newPicoPwm(pwm pwmGroup, pin machine.Pin, mode picoPwmMode) *picoPwm { p := &picoPwm{ - pwm: pwm, - pin: pin, + pwm: pwm, + pin: pin, + period: rev0.DefaultPWMPeriod, cal: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ { - Input: rev1.MinOutputVoltage, - Output: rev1.CalibratedTop, + Input: rev0.MinOutputVoltage, + Output: rev0.CalibratedTop, }, { - Input: rev1.MaxOutputVoltage, - Output: rev1.CalibratedOffset, + Input: rev0.MaxOutputVoltage, + Output: rev0.CalibratedOffset, }, }), } @@ -56,8 +68,12 @@ 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(config.Period.Nanoseconds()), + Period: uint64(p.period.Nanoseconds()), }) if err != nil { return fmt.Errorf("pwm Configure error: %w", err) @@ -74,10 +90,17 @@ func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error { } p.ch = ch + p.wavefold = config.PerformWavefold + return nil } func (p *picoPwm) Set(v float32) { + if p.wavefold { + if v < 0.0 { + v = -v + } + } volts := p.cal.Remap(v) state := interrupt.Disable() p.pwm.Set(p.ch, uint32(volts)) diff --git a/internal/projects/clockgenerator/screen/main.go b/internal/projects/clockgenerator/screen/main.go index 3d102c8..faa5cf2 100644 --- a/internal/projects/clockgenerator/screen/main.go +++ b/internal/projects/clockgenerator/screen/main.go @@ -28,7 +28,7 @@ var ( func (m *Main) Start(e *europi.EuroPi) { m.writer = fontwriter.Writer{ - Display: e.Display, + Display: e.OLED, Font: DefaultFont, } } @@ -43,7 +43,7 @@ func (m *Main) Button1(e *europi.EuroPi, deltaTime time.Duration) { func (m *Main) Paint(e *europi.EuroPi, deltaTime time.Duration) { if m.Clock.Enabled() { - tinydraw.Line(e.Display, 0, 0, 7, 0, draw.White) + tinydraw.Line(e.OLED, 0, 0, 7, 0, draw.White) } m.writer.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y, draw.White) m.writer.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y, draw.White) diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index ae77b58..426cc9e 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -162,11 +162,11 @@ func (c *Clockwerk) clock(i uint8, reset chan uint8) { high, low := c.clockPulseWidth(c.clocks[i]) - c.CV[i].SetCV(1.0) + c.CV()[i].SetCV(1.0) t = t.Add(high) time.Sleep(time.Since(t)) - c.CV[i].SetCV(0.0) + c.CV()[i].SetCV(0.0) t = t.Add(low) time.Sleep(time.Since(t)) } @@ -195,7 +195,7 @@ func (c *Clockwerk) updateDisplay() { return } c.displayShouldUpdate = false - c.Display.ClearBuffer() + c.OLED.ClearBuffer() // Master clock and pulse width. var external string @@ -205,7 +205,7 @@ func (c *Clockwerk) updateDisplay() { c.writer.WriteLine(external+"BPM: "+strconv.Itoa(int(c.bpm)), 2, 8, draw.White) // Display each clock multiplication or division setting. - dispWidth, _ := c.Display.Size() + dispWidth, _ := c.OLED.Size() divWidth := int(dispWidth) / len(c.clocks) for i, factor := range c.clocks { text := " 1" @@ -220,9 +220,9 @@ func (c *Clockwerk) updateDisplay() { xWidth := int16(divWidth) xOffset := int16(c.selected) * xWidth // TODO: replace box with chevron. - _ = tinydraw.Rectangle(c.Display, xOffset, 16, xWidth, 16, draw.White) + _ = tinydraw.Rectangle(c.OLED, xOffset, 16, xWidth, 16, draw.White) - _ = c.Display.Display() + _ = c.OLED.Display() } var app Clockwerk @@ -232,7 +232,7 @@ func appStart(e *europi.EuroPi) { app.clocks = DefaultFactor app.displayShouldUpdate = true app.writer = fontwriter.Writer{ - Display: e.Display, + Display: e.OLED, Font: DefaultFont, } app.bpmLerp = lerp.NewLerp32[uint16](MinBPM-1, MaxBPM) @@ -288,7 +288,12 @@ func mainLoop() { } func main() { - appStart(europi.New()) + e, _ := europi.New().(*europi.EuroPi) + if e == nil { + panic("europi not detected") + } + + appStart(e) // Check for clock updates every 2 seconds. ticker := time.NewTicker(ResetDelay) diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index d1f7f44..e32f047 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -41,13 +41,13 @@ var ( ) func mainLoop(e *europi.EuroPi) { - e.Display.ClearBuffer() + e.OLED.ClearBuffer() // Highlight the border of the oled display. - _ = tinydraw.Rectangle(e.Display, 0, 0, 128, 32, draw.White) + _ = tinydraw.Rectangle(e.OLED, 0, 0, 128, 32, draw.White) writer := fontwriter.Writer{ - Display: e.Display, + Display: e.OLED, Font: DefaultFont, } @@ -67,7 +67,7 @@ func mainLoop(e *europi.EuroPi) { // Show current button press state. writer.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", e.B1.Value(), e.B2.Value()), 3, 28, draw.White) - _ = e.Display.Display() + _ = e.OLED.Display() // Set voltage values for the 6 CV outputs. if kv := uint16(e.K1.Percent() * float32(1<<12)); kv != myApp.prevK1 { @@ -88,7 +88,11 @@ func mainLoop(e *europi.EuroPi) { } func main() { - e := europi.New() + e, _ := europi.New().(*europi.EuroPi) + if e == nil { + panic("europi not detected") + } + appStart(e) for { mainLoop(e) diff --git a/internal/projects/randomskips/screen/main.go b/internal/projects/randomskips/screen/main.go index 87eaabb..128f6cd 100644 --- a/internal/projects/randomskips/screen/main.go +++ b/internal/projects/randomskips/screen/main.go @@ -30,7 +30,7 @@ var ( func (m *Main) Start(e *europi.EuroPi) { m.writer = fontwriter.Writer{ - Display: e.Display, + Display: e.OLED, Font: DefaultFont, } } @@ -45,7 +45,7 @@ func (m *Main) Button1(e *europi.EuroPi, deltaTime time.Duration) { func (m *Main) Paint(e *europi.EuroPi, deltaTime time.Duration) { if m.Clock.Enabled() { - tinydraw.Line(e.Display, 0, 0, 7, 0, draw.White) + tinydraw.Line(m.writer.Display, 0, 0, 7, 0, draw.White) } m.writer.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y, draw.White) m.writer.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y, draw.White) diff --git a/lerp/remap.go b/lerp/remap.go index f79202e..614d400 100644 --- a/lerp/remap.go +++ b/lerp/remap.go @@ -6,6 +6,7 @@ type Remapable interface { type Remapper[TIn, TOut Remapable, F Float] interface { Remap(value TIn) TOut + Unmap(value TOut) TIn MCoeff() F InputMinimum() TIn InputMaximum() TIn diff --git a/lerp/remap32.go b/lerp/remap32.go index 4134ed8..cac69df 100644 --- a/lerp/remap32.go +++ b/lerp/remap32.go @@ -4,31 +4,47 @@ type remap32[TIn, TOut Remapable] struct { inMin TIn inMax TIn outMin TOut + outMax TOut r float32 } func NewRemap32[TIn, TOut Remapable](inMin, inMax TIn, outMin, outMax TOut) Remapper32[TIn, TOut] { var r float32 // if rIn is 0, then we don't need to test further, we're always min (max) value - if rIn := inMax - inMin; rIn != 0 { - if rOut := outMax - outMin; rOut != 0 { - r = float32(rOut) / float32(rIn) + if rIn := float32(inMax) - float32(inMin); rIn != 0 { + if rOut := float32(outMax) - float32(outMin); rOut != 0 { + r = rOut / rIn } } return remap32[TIn, TOut]{ inMin: inMin, inMax: inMax, outMin: outMin, + outMax: outMax, r: r, } } func (r remap32[TIn, TOut]) Remap(value TIn) TOut { - if r.r == 0.0 { + switch { + case r.r == 0.0: + return r.outMin + case value == r.inMin: return r.outMin + case value == r.inMax: + return r.outMax + default: + return r.outMin + TOut(r.r*float32(value-r.inMin)) + } +} + +func (r remap32[TIn, TOut]) Unmap(value TOut) TIn { + if r.r == 0.0 { + return r.inMax } - return r.outMin + TOut(r.r*float32(value-r.inMin)) + rOut := float32(value) - float32(r.outMin) + return r.inMin + TIn(rOut/r.r) } func (r remap32[TIn, TOut]) MCoeff() float32 { diff --git a/lerp/remap32_test.go b/lerp/remap32_test.go index e0b50d4..d5109e3 100644 --- a/lerp/remap32_test.go +++ b/lerp/remap32_test.go @@ -67,6 +67,60 @@ func TestRemap32(t *testing.T) { }) }) + 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("MCoeff", func(t *testing.T) { t.Run("ZeroRange", func(t *testing.T) { inMin, inMax := 10, 10 diff --git a/lerp/remap64.go b/lerp/remap64.go index e25854f..954910c 100644 --- a/lerp/remap64.go +++ b/lerp/remap64.go @@ -4,31 +4,47 @@ type remap64[TIn, TOut Remapable] struct { inMin TIn inMax TIn outMin TOut + outMax TOut r float64 } func NewRemap64[TIn, TOut Remapable](inMin, inMax TIn, outMin, outMax TOut) Remapper64[TIn, TOut] { var r float64 // if rIn is 0, then we don't need to test further, we're always min (max) value - if rIn := inMax - inMin; rIn != 0 { - if rOut := outMax - outMin; rOut != 0 { - r = float64(rOut) / float64(rIn) + if rIn := float64(inMax) - float64(inMin); rIn != 0 { + if rOut := float64(outMax) - float64(outMin); rOut != 0 { + r = rOut / rIn } } return remap64[TIn, TOut]{ inMin: inMin, inMax: inMax, outMin: outMin, + outMax: outMax, r: r, } } func (r remap64[TIn, TOut]) Remap(value TIn) TOut { - if r.r == 0.0 { + switch { + case r.r == 0.0: + return r.outMin + case value == r.inMin: return r.outMin + case value == r.inMax: + return r.outMax + default: + return r.outMin + TOut(r.r*float64(value-r.inMin)) + } +} + +func (r remap64[TIn, TOut]) Unmap(value TOut) TIn { + if r.r == 0.0 { + return r.inMax } - return r.outMin + TOut(r.r*float64(value-r.inMin)) + rOut := float64(value) - float64(r.outMin) + return r.inMin + TIn(rOut/r.r) } func (r remap64[TIn, TOut]) MCoeff() float64 { diff --git a/lerp/remap64_test.go b/lerp/remap64_test.go index 369836e..3877056 100644 --- a/lerp/remap64_test.go +++ b/lerp/remap64_test.go @@ -67,6 +67,60 @@ func TestRemap64(t *testing.T) { }) }) + 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) + // while the 32-bit version of this test truncates down to -1 after some error, + // there's enough information available in a 64-bit float where it properly + // calculates -2 + 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("MCoeff", func(t *testing.T) { t.Run("ZeroRange", func(t *testing.T) { inMin, inMax := 10, 10 diff --git a/lerp/remappoint.go b/lerp/remappoint.go index 28152dd..66726c9 100644 --- a/lerp/remappoint.go +++ b/lerp/remappoint.go @@ -27,6 +27,11 @@ func (r remapPoint[TIn, TOut, TFloat]) Remap(value TIn) TOut { return r.out } +func (r remapPoint[TIn, TOut, TFloat]) Unmap(value TOut) TIn { + // `value` isn't used here - just return `in` + return r.in +} + func (r remapPoint[TIn, TOut, TFloat]) MCoeff() TFloat { return 0.0 } diff --git a/lerp/remappoint_test.go b/lerp/remappoint_test.go index a974057..e7a6a20 100644 --- a/lerp/remappoint_test.go +++ b/lerp/remappoint_test.go @@ -58,6 +58,35 @@ func TestRemapPoint(t *testing.T) { }) }) + t.Run("Unmap", func(t *testing.T) { + t.Run("InRange", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := in, l.Unmap(out); actual != expected { + t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + + t.Run("OutOfRange", func(t *testing.T) { + // Unmap() will work just reply with the "in" point when operating out of range + t.Run("BelowMin", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := in, l.Unmap(out-2); actual != expected { + t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + + t.Run("AboveMax", func(t *testing.T) { + in, out := 0, float32(math.Pi) + l := lerp.NewRemapPoint32(in, out) + if expected, actual := in, l.Unmap(out+2); actual != expected { + t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) + } + }) + }) + }) + t.Run("MCoeff", func(t *testing.T) { in, out := 0, float32(math.Pi) l := lerp.NewRemapPoint32(in, out) From 6406331c4f230df409796ce8261204de2dc74099 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 4 May 2023 07:50:35 -0700 Subject: [PATCH 48/62] fix for panic on wrong hw type conversion --- bootstrapoptions_app_conversion.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bootstrapoptions_app_conversion.go b/bootstrapoptions_app_conversion.go index aeae862..5c4efea 100644 --- a/bootstrapoptions_app_conversion.go +++ b/bootstrapoptions_app_conversion.go @@ -1,6 +1,7 @@ package europi import ( + "errors" "time" ) @@ -81,6 +82,9 @@ func (a *appWrapper[THardware]) ApplicationStart() AppStartFunc { func (a *appWrapper[THardware]) doStart(e Hardware) { pi, _ := e.(THardware) + if any(pi) == nil { + panic(errors.New("incorrect hardware type conversion")) + } a.start.Start(pi) } @@ -93,6 +97,9 @@ func (a *appWrapper[THardware]) ApplicationMainLoop() AppMainLoopFunc { func (a *appWrapper[THardware]) doMainLoop(e Hardware, deltaTime time.Duration) { pi, _ := e.(THardware) + if any(pi) == nil { + panic(errors.New("incorrect hardware type conversion")) + } a.mainLoop.MainLoop(pi, deltaTime) } @@ -104,8 +111,9 @@ func (a *appWrapper[THardware]) ApplicationEnd() AppEndFunc { } func (a *appWrapper[THardware]) doEnd(e Hardware) { - if a.end != nil { - pi, _ := e.(THardware) - a.end.End(pi) + pi, _ := e.(THardware) + if any(pi) == nil { + panic(errors.New("incorrect hardware type conversion")) } + a.end.End(pi) } From 0dc66ab5164ac50bf6b29dfd085f7daa0f2bacdf Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 4 May 2023 13:39:27 -0700 Subject: [PATCH 49/62] Fix issues with invalid setup during simulation --- experimental/envelope/map32.go | 4 ++-- experimental/envelope/map64.go | 4 ++-- internal/nonpico/rev0/listeners.go | 17 +++++++++++++++++ internal/nonpico/rev0/wsactivation.go | 3 +++ internal/nonpico/rev1/listeners.go | 24 ++++++++++++++++++++++++ internal/nonpico/rev1/wsactivation.go | 2 ++ 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/experimental/envelope/map32.go b/experimental/envelope/map32.go index d6442f2..6ef00a0 100644 --- a/experimental/envelope/map32.go +++ b/experimental/envelope/map32.go @@ -76,13 +76,13 @@ func (m *envMap32[TIn, TOut]) Unmap(value TOut) TIn { if outMin < outMax { if value < outMin { return r.InputMinimum() - } else if value < outMax { + } else if value <= outMax { return r.Unmap(value) } } else { if value < outMax { return r.InputMinimum() - } else if value < outMin { + } else if value <= outMin { return r.Unmap(value) } } diff --git a/experimental/envelope/map64.go b/experimental/envelope/map64.go index 3d5aaed..8aab14f 100644 --- a/experimental/envelope/map64.go +++ b/experimental/envelope/map64.go @@ -76,13 +76,13 @@ func (m *envMap64[TIn, TOut]) Unmap(value TOut) TIn { if outMin < outMax { if value < outMin { return r.InputMinimum() - } else if value < outMax { + } else if value <= outMax { return r.Unmap(value) } } else { if value < outMax { return r.InputMinimum() - } else if value < outMin { + } else if value <= outMin { return r.Unmap(value) } } diff --git a/internal/nonpico/rev0/listeners.go b/internal/nonpico/rev0/listeners.go index 3aa52bf..abaebfc 100644 --- a/internal/nonpico/rev0/listeners.go +++ b/internal/nonpico/rev0/listeners.go @@ -18,6 +18,23 @@ var ( bus = event.NewBus() ) +func setupDefaultState() { + bus.Post(fmt.Sprintf("hw_value_%d", rev0.HardwareIdButton1Input), common.HwMessageDigitalValue{ + Value: false, + }) + bus.Post(fmt.Sprintf("hw_value_%d", rev0.HardwareIdButton2Input), common.HwMessageDigitalValue{ + Value: false, + }) + + bus.Post(fmt.Sprintf("hw_value_%d", rev0.HardwareIdKnob1Input), common.HwMessageADCValue{ + Value: aiLerp.Lerp(0.5), + }) + + bus.Post(fmt.Sprintf("hw_value_%d", rev0.HardwareIdKnob2Input), common.HwMessageADCValue{ + Value: aiLerp.Lerp(0.5), + }) +} + func setupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) { ids := []hal.HardwareId{ rev0.HardwareIdAnalog1Output, diff --git a/internal/nonpico/rev0/wsactivation.go b/internal/nonpico/rev0/wsactivation.go index bda381f..27ec08e 100644 --- a/internal/nonpico/rev0/wsactivation.go +++ b/internal/nonpico/rev0/wsactivation.go @@ -42,6 +42,9 @@ 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() diff --git a/internal/nonpico/rev1/listeners.go b/internal/nonpico/rev1/listeners.go index 72a0e04..a9a8993 100644 --- a/internal/nonpico/rev1/listeners.go +++ b/internal/nonpico/rev1/listeners.go @@ -18,6 +18,30 @@ var ( bus = event.NewBus() ) +func setupDefaultState() { + bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdDigital1Input), common.HwMessageDigitalValue{ + Value: false, + }) + bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdAnalog1Input), common.HwMessageADCValue{ + Value: rev1.DefaultCalibratedMaxAI, + }) + + bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdButton1Input), common.HwMessageDigitalValue{ + Value: false, + }) + bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdButton2Input), common.HwMessageDigitalValue{ + Value: false, + }) + + bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdKnob1Input), common.HwMessageADCValue{ + Value: aiLerp.Lerp(0.5), + }) + + bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdKnob2Input), common.HwMessageADCValue{ + Value: aiLerp.Lerp(0.5), + }) +} + func setupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) { for id := hal.HardwareIdVoltage1Output; id <= hal.HardwareIdVoltage6Output; id++ { fn := func(hid hal.HardwareId) func(common.HwMessagePwmValue) { diff --git a/internal/nonpico/rev1/wsactivation.go b/internal/nonpico/rev1/wsactivation.go index 969a6e3..57cff7f 100644 --- a/internal/nonpico/rev1/wsactivation.go +++ b/internal/nonpico/rev1/wsactivation.go @@ -45,6 +45,8 @@ var nonPicoSiteContent embed.FS func (a *WSActivation) Start(ctx context.Context) { a.ctx, a.cancel = context.WithCancel(ctx) + setupDefaultState() + go func() { defer a.cancel() From 80602a5b6c41dfd063be194f36ee9ba84f029a56 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 5 May 2023 08:35:03 -0700 Subject: [PATCH 50/62] Fix for rev0 params mismatch --- hardware/platform.go | 6 ++++-- internal/nonpico/wsactivator.go | 1 + internal/pico/pico.go | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/hardware/platform.go b/hardware/platform.go index bd4c6db..653e588 100644 --- a/hardware/platform.go +++ b/hardware/platform.go @@ -5,6 +5,7 @@ import ( "sync/atomic" "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/hardware/rev0" "github.com/awonak/EuroPiGo/hardware/rev1" ) @@ -13,14 +14,15 @@ import ( func GetHardware[T any](revision hal.Revision, id hal.HardwareId) T { switch revision { case hal.Revision0: - return rev1.GetHardware[T](id) + return rev0.GetHardware[T](id) case hal.Revision1: return rev1.GetHardware[T](id) case hal.Revision2: // TODO: implement hardware design of rev2 - return rev1.GetHardware[T](id) + //return rev2.GetHardware[T](id) + fallthrough default: var none T diff --git a/internal/nonpico/wsactivator.go b/internal/nonpico/wsactivator.go index d6e415b..a4309df 100644 --- a/internal/nonpico/wsactivator.go +++ b/internal/nonpico/wsactivator.go @@ -21,6 +21,7 @@ func ActivateWebSocket(ctx context.Context, revision hal.Revision) WSActivation return rev0.ActivateWebSocket(ctx) case hal.Revision1: return rev1.ActivateWebSocket(ctx) + // TODO: add rev2 default: return nil } diff --git a/internal/pico/pico.go b/internal/pico/pico.go index a406610..3318c49 100644 --- a/internal/pico/pico.go +++ b/internal/pico/pico.go @@ -8,25 +8,25 @@ import ( "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 -// Rev0 leverages Rev1 layout and functions func initRevision0() { - rev1.Initialize(rev1.InitializationParameters{ + rev0.Initialize(rev0.InitializationParameters{ InputButton1: newPicoDigitalReader(machine.GPIO15), InputButton2: newPicoDigitalReader(machine.GPIO18), InputKnob1: newPicoAdc(machine.ADC2), InputKnob2: newPicoAdc(machine.ADC1), - OutputVoltage1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeDigitalRevision0), - OutputVoltage2: newPicoPwm(machine.PWM3, machine.GPIO22, picoPwmModeDigitalRevision0), - OutputVoltage3: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeDigitalRevision0), - OutputVoltage4: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeDigitalRevision0), - OutputVoltage5: newPicoPwm(machine.PWM7, machine.GPIO14, picoPwmModeAnalogRevision0), - OutputVoltage6: newPicoPwm(machine.PWM5, machine.GPIO11, picoPwmModeAnalogRevision0), - OutputVoltage7: newPicoPwm(machine.PWM5, machine.GPIO10, picoPwmModeAnalogRevision0), - OutputVoltage8: newPicoPwm(machine.PWM3, machine.GPIO7, picoPwmModeAnalogRevision0), + OutputAnalog1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeDigitalRevision0), + OutputAnalog2: newPicoPwm(machine.PWM3, machine.GPIO22, picoPwmModeDigitalRevision0), + OutputAnalog3: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeDigitalRevision0), + OutputAnalog4: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeDigitalRevision0), + OutputDigital1: newPicoPwm(machine.PWM7, machine.GPIO14, picoPwmModeAnalogRevision0), + OutputDigital2: newPicoPwm(machine.PWM5, machine.GPIO11, picoPwmModeAnalogRevision0), + OutputDigital3: newPicoPwm(machine.PWM5, machine.GPIO10, picoPwmModeAnalogRevision0), + OutputDigital4: newPicoPwm(machine.PWM3, machine.GPIO7, picoPwmModeAnalogRevision0), DeviceRandomGenerator1: &picoRnd{}, }) } From ba2d559cafb2198a7aacc703366598eb507a3de4 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 5 May 2023 13:57:06 -0700 Subject: [PATCH 51/62] move bootstrap into its own folder - simplify startup process - make detection more manual (require calling EnsureHardware()) - this prevents major init() leaks and race conditions - clean up screen/ui hardware detection - reduce required components to build to minimums - target europi prototype with build flag --- bootstrap.go => bootstrap/bootstrap.go | 42 +++--- .../features.go | 22 +-- .../lifecycle.go | 18 ++- bootstrap_nonpico.go => bootstrap/nonpico.go | 8 +- .../nonpico_panic.go | 2 +- bootstrap/nonpico_websimenabled.go | 8 ++ bootstrapoptions.go => bootstrap/options.go | 8 +- .../options_app.go | 12 +- .../options_app_conversion.go | 47 ++++--- .../options_features.go | 18 +-- .../options_lifecycle.go | 22 +-- .../options_ui.go | 8 +- bootstrap_panic.go => bootstrap/panic.go | 13 +- .../pico_panicdisabled.go | 2 +- .../pico_panicenabled.go | 2 +- bootstrap_ui.go => bootstrap/ui.go | 28 ++-- .../uimodule.go | 51 +++---- europi.go | 3 + europi_nonpico.go | 18 +++ europi_pico.go | 18 +++ experimental/screenbank/screenbank.go | 16 +-- experimental/screenbank/screenbankentry.go | 15 +- experimental/screenbank/screenbankoptions.go | 131 ++++++++++-------- hardware/common/contextpi.go | 23 +++ hardware/hal/hardware.go | 3 + hardware/platform.go | 51 ++----- hardware/rev0/platform.go | 12 ++ hardware/rev0/voltageoutput.go | 8 +- hardware/rev1/platform.go | 12 ++ hardware/rev1/voltageoutput.go | 8 +- hardware/revisiondetection.go | 42 ++++++ internal/nonpico/nonpico.go | 6 + internal/nonpico/rev0.go | 3 +- internal/nonpico/rev1.go | 3 +- internal/nonpico/rev2.go | 3 +- internal/nonpico/ws/websocket.go | 2 +- internal/pico/revisiondetection.go | 9 +- internal/pico/revisiondetection_rev0.go | 9 ++ .../projects/clockgenerator/clockgenerator.go | 20 +-- internal/projects/clockwerk/clockwerk.go | 8 ++ internal/projects/diagnostics/diagnostics.go | 8 ++ internal/projects/randomskips/randomskips.go | 18 ++- revisiondetection.go | 7 + 43 files changed, 472 insertions(+), 295 deletions(-) rename bootstrap.go => bootstrap/bootstrap.go (84%) rename bootstrap_features.go => bootstrap/features.go (58%) rename bootstrap_lifecycle.go => bootstrap/lifecycle.go (55%) rename bootstrap_nonpico.go => bootstrap/nonpico.go (64%) rename bootstrap_nonpico_panic.go => bootstrap/nonpico_panic.go (83%) create mode 100644 bootstrap/nonpico_websimenabled.go rename bootstrapoptions.go => bootstrap/options.go (81%) rename bootstrapoptions_app.go => bootstrap/options_app.go (86%) rename bootstrapoptions_app_conversion.go => bootstrap/options_app_conversion.go (61%) rename bootstrapoptions_features.go => bootstrap/options_features.go (81%) rename bootstrapoptions_lifecycle.go => bootstrap/options_lifecycle.go (88%) rename bootstrapoptions_ui.go => bootstrap/options_ui.go (85%) rename bootstrap_panic.go => bootstrap/panic.go (75%) rename bootstrap_pico_panicdisabled.go => bootstrap/pico_panicdisabled.go (95%) rename bootstrap_pico_panicenabled.go => bootstrap/pico_panicenabled.go (87%) rename bootstrap_ui.go => bootstrap/ui.go (54%) rename bootstrap_uimodule.go => bootstrap/uimodule.go (63%) create mode 100644 europi_nonpico.go create mode 100644 europi_pico.go create mode 100644 hardware/common/contextpi.go create mode 100644 hardware/revisiondetection.go create mode 100644 internal/pico/revisiondetection_rev0.go create mode 100644 revisiondetection.go diff --git a/bootstrap.go b/bootstrap/bootstrap.go similarity index 84% rename from bootstrap.go rename to bootstrap/bootstrap.go index e2f17f6..7dd7ca0 100644 --- a/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -1,27 +1,34 @@ -package europi +package bootstrap import ( "context" "errors" "sync" "time" + + europi "github.com/awonak/EuroPiGo" ) var ( // Pi is a global EuroPi instance constructed by calling the Bootstrap() function - Pi Hardware + Pi europi.Hardware piWantDestroyChan chan any ) // Bootstrap will set up a global runtime environment (see europi.Pi) -func Bootstrap(options ...BootstrapOption) error { +func Bootstrap(pi europi.Hardware, options ...BootstrapOption) error { + e := pi + if e == nil { + return errors.New("europi must be provided") + } + config := bootstrapConfig{ panicHandler: DefaultPanicHandler, enableDisplayLogger: DefaultEnableDisplayLogger, initRandom: DefaultInitRandom, - enableNonPicoWebSocket: false, - europi: nil, + enableNonPicoWebSocket: defaultWebSimEnabled, + europi: e, appConfig: bootstrapAppConfig{ mainLoopInterval: DefaultAppMainLoopInterval, @@ -64,15 +71,6 @@ func Bootstrap(options ...BootstrapOption) error { } } - if config.europi == nil { - config.europi = New() - } - e := config.europi - - if e == nil { - return errors.New("no europi available") - } - Pi = e piWantDestroyChan = make(chan any, 1) @@ -82,12 +80,12 @@ func Bootstrap(options ...BootstrapOption) error { ) panicHandler := config.panicHandler lastDestroyFunc := config.onBeginDestroyFn - ctx, cancel := context.WithCancel(context.TODO()) + ctx, cancel := context.WithCancel(e.Context()) runBootstrapDestroy := func() { reason := recover() cancel() if reason != nil && panicHandler != nil { - config.onBeginDestroyFn = func(e Hardware, reason any) { + config.onBeginDestroyFn = func(e europi.Hardware, reason any) { if lastDestroyFunc != nil { lastDestroyFunc(e, reason) } @@ -124,7 +122,7 @@ func Shutdown(reason any) error { return nil } -func bootstrapInitializeComponents(ctx context.Context, config *bootstrapConfig, e Hardware) NonPicoWSActivation { +func bootstrapInitializeComponents(ctx context.Context, config *bootstrapConfig, e europi.Hardware) NonPicoWSActivation { if config.onPreInitializeComponentsFn != nil { config.onPreInitializeComponentsFn(e) } @@ -154,7 +152,7 @@ func bootstrapInitializeComponents(ctx context.Context, config *bootstrapConfig, return nonPicoWSApi } -func bootstrapRunLoop(config *bootstrapConfig, e Hardware) { +func bootstrapRunLoop(config *bootstrapConfig, e europi.Hardware) { if config.appConfig.onAppStartFn != nil { config.appConfig.onAppStartFn(e) } @@ -174,7 +172,7 @@ func bootstrapRunLoop(config *bootstrapConfig, e Hardware) { } } -func bootstrapRunLoopWithDelay(config *bootstrapConfig, e Hardware) { +func bootstrapRunLoopWithDelay(config *bootstrapConfig, e europi.Hardware) { if config.appConfig.onAppMainLoopFn == nil { panic(errors.New("no main loop specified")) } @@ -195,7 +193,7 @@ func bootstrapRunLoopWithDelay(config *bootstrapConfig, e Hardware) { } } -func bootstrapRunLoopNoDelay(config *bootstrapConfig, e Hardware) { +func bootstrapRunLoopNoDelay(config *bootstrapConfig, e europi.Hardware) { if config.appConfig.onAppMainLoopFn == nil { panic(errors.New("no main loop specified")) } @@ -214,7 +212,7 @@ func bootstrapRunLoopNoDelay(config *bootstrapConfig, e Hardware) { } } -func bootstrapDestroy(config *bootstrapConfig, e Hardware, nonPicoWSApi NonPicoWSActivation, reason any) { +func bootstrapDestroy(config *bootstrapConfig, e europi.Hardware, nonPicoWSApi NonPicoWSActivation, reason any) { if config.onBeginDestroyFn != nil { config.onBeginDestroyFn(e, reason) } @@ -229,7 +227,7 @@ func bootstrapDestroy(config *bootstrapConfig, e Hardware, nonPicoWSApi NonPicoW uninitRandom(e) - if display := Display(e); display != nil { + if display := europi.Display(e); display != nil { // show the last buffer _ = display.Display() } diff --git a/bootstrap_features.go b/bootstrap/features.go similarity index 58% rename from bootstrap_features.go rename to bootstrap/features.go index 34d7112..72287f9 100644 --- a/bootstrap_features.go +++ b/bootstrap/features.go @@ -1,10 +1,11 @@ -package europi +package bootstrap import ( "context" "log" "os" + europi "github.com/awonak/EuroPiGo" "github.com/awonak/EuroPiGo/experimental/displaylogger" "github.com/awonak/EuroPiGo/hardware/hal" ) @@ -13,13 +14,13 @@ var ( dispLog displaylogger.Logger ) -func enableDisplayLogger(e Hardware) { +func enableDisplayLogger(e europi.Hardware) { if dispLog != nil { // already enabled - can happen when panicking return } - display := Display(e) + display := europi.Display(e) if display == nil { // no display, can't continue return @@ -30,38 +31,39 @@ func enableDisplayLogger(e Hardware) { log.SetOutput(dispLog) } -func disableDisplayLogger(e Hardware) { +func disableDisplayLogger(e europi.Hardware) { flushDisplayLogger(e) dispLog = nil log.SetOutput(os.Stdout) } -func flushDisplayLogger(e Hardware) { +func flushDisplayLogger(e europi.Hardware) { if dispLog != nil { dispLog.Flush() } } -func initRandom(e Hardware) { +func initRandom(e europi.Hardware) { if rnd := e.Random(); rnd != nil { _ = rnd.Configure(hal.RandomGeneratorConfig{}) } } -func uninitRandom(e Hardware) { +func uninitRandom(e europi.Hardware) { } // used for non-pico testing of bootstrapped europi apps var ( - activateNonPicoWebSocket func(ctx context.Context, e Hardware) NonPicoWSActivation - deactivateNonPicoWebSocket func(e Hardware, api NonPicoWSActivation) + defaultWebSimEnabled bool + activateNonPicoWebSocket func(ctx context.Context, e europi.Hardware) NonPicoWSActivation + deactivateNonPicoWebSocket func(e europi.Hardware, api NonPicoWSActivation) ) type NonPicoWSActivation interface { Shutdown() error } -func ActivateNonPicoWS(ctx context.Context, e Hardware) NonPicoWSActivation { +func ActivateNonPicoWS(ctx context.Context, e europi.Hardware) NonPicoWSActivation { if activateNonPicoWebSocket == nil { return nil } diff --git a/bootstrap_lifecycle.go b/bootstrap/lifecycle.go similarity index 55% rename from bootstrap_lifecycle.go rename to bootstrap/lifecycle.go index 71fbd50..b40667b 100644 --- a/bootstrap_lifecycle.go +++ b/bootstrap/lifecycle.go @@ -1,9 +1,13 @@ -package europi +package bootstrap -import "time" +import ( + "time" -func DefaultPostBootstrapInitialization(e Hardware) { - display := Display(e) + europi "github.com/awonak/EuroPiGo" +) + +func DefaultPostBootstrapInitialization(e europi.Hardware) { + display := europi.Display(e) if display == nil { // no display, can't continue return @@ -15,8 +19,8 @@ func DefaultPostBootstrapInitialization(e Hardware) { } } -func DefaultBootstrapCompleted(e Hardware) { - display := Display(e) +func DefaultBootstrapCompleted(e europi.Hardware) { + display := europi.Display(e) if display == nil { // no display, can't continue return @@ -29,5 +33,5 @@ func DefaultBootstrapCompleted(e Hardware) { } // DefaultMainLoop is the default main loop used if a new one is not specified to Bootstrap() -func DefaultMainLoop(e Hardware, deltaTime time.Duration) { +func DefaultMainLoop(e europi.Hardware, deltaTime time.Duration) { } diff --git a/bootstrap_nonpico.go b/bootstrap/nonpico.go similarity index 64% rename from bootstrap_nonpico.go rename to bootstrap/nonpico.go index ece44b8..a31508d 100644 --- a/bootstrap_nonpico.go +++ b/bootstrap/nonpico.go @@ -1,20 +1,21 @@ //go:build !pico // +build !pico -package europi +package bootstrap import ( "context" + europi "github.com/awonak/EuroPiGo" "github.com/awonak/EuroPiGo/internal/nonpico" ) -func nonPicoActivateWebSocket(ctx context.Context, e Hardware) NonPicoWSActivation { +func nonPicoActivateWebSocket(ctx context.Context, e europi.Hardware) NonPicoWSActivation { nonPicoWSApi := nonpico.ActivateWebSocket(ctx, e.Revision()) return nonPicoWSApi } -func nonPicoDeactivateWebSocket(e Hardware, nonPicoWSApi NonPicoWSActivation) { +func nonPicoDeactivateWebSocket(e europi.Hardware, nonPicoWSApi NonPicoWSActivation) { if nonPicoWSApi != nil { if err := nonPicoWSApi.Shutdown(); err != nil { panic(err) @@ -25,4 +26,5 @@ func nonPicoDeactivateWebSocket(e Hardware, nonPicoWSApi NonPicoWSActivation) { func init() { activateNonPicoWebSocket = nonPicoActivateWebSocket deactivateNonPicoWebSocket = nonPicoDeactivateWebSocket + } diff --git a/bootstrap_nonpico_panic.go b/bootstrap/nonpico_panic.go similarity index 83% rename from bootstrap_nonpico_panic.go rename to bootstrap/nonpico_panic.go index 1437632..c9d4f53 100644 --- a/bootstrap_nonpico_panic.go +++ b/bootstrap/nonpico_panic.go @@ -1,7 +1,7 @@ //go:build !pico // +build !pico -package europi +package bootstrap func init() { DefaultPanicHandler = handlePanicLogger diff --git a/bootstrap/nonpico_websimenabled.go b/bootstrap/nonpico_websimenabled.go new file mode 100644 index 0000000..ead2b1d --- /dev/null +++ b/bootstrap/nonpico_websimenabled.go @@ -0,0 +1,8 @@ +//go:build !pico && websim +// +build !pico,websim + +package bootstrap + +func init() { + defaultWebSimEnabled = true +} diff --git a/bootstrapoptions.go b/bootstrap/options.go similarity index 81% rename from bootstrapoptions.go rename to bootstrap/options.go index 89f58b1..437609c 100644 --- a/bootstrapoptions.go +++ b/bootstrap/options.go @@ -1,13 +1,15 @@ -package europi +package bootstrap + +import europi "github.com/awonak/EuroPiGo" // BootstrapOption is a single configuration parameter passed to the Bootstrap() function type BootstrapOption func(o *bootstrapConfig) error type bootstrapConfig struct { - panicHandler func(e Hardware, reason any) + panicHandler func(e europi.Hardware, reason any) enableDisplayLogger bool initRandom bool - europi Hardware + europi europi.Hardware enableNonPicoWebSocket bool // application diff --git a/bootstrapoptions_app.go b/bootstrap/options_app.go similarity index 86% rename from bootstrapoptions_app.go rename to bootstrap/options_app.go index b1d80d8..3ac2379 100644 --- a/bootstrapoptions_app.go +++ b/bootstrap/options_app.go @@ -1,19 +1,21 @@ -package europi +package bootstrap import ( "errors" "time" + + europi "github.com/awonak/EuroPiGo" ) -type ApplicationStart[THardware Hardware] interface { +type ApplicationStart[THardware europi.Hardware] interface { Start(e THardware) } -type ApplicationMainLoop[THardware Hardware] interface { +type ApplicationMainLoop[THardware europi.Hardware] interface { MainLoop(e THardware, deltaTime time.Duration) } -type ApplicationEnd[THardware Hardware] interface { +type ApplicationEnd[THardware europi.Hardware] interface { End(e THardware) } @@ -25,7 +27,7 @@ func App(app any, opts ...BootstrapAppOption) BootstrapOption { } // automatically divine the functions for the app - start, mainLoop, end := getAppFuncs(app) + start, mainLoop, end := getAppFuncs(o.europi, app) if start == nil && mainLoop == nil && end == nil { return errors.New("app must provide at least one application function interface (ApplicationStart, ApplicationMainLoop, ApplicationEnd)") diff --git a/bootstrapoptions_app_conversion.go b/bootstrap/options_app_conversion.go similarity index 61% rename from bootstrapoptions_app_conversion.go rename to bootstrap/options_app_conversion.go index 5c4efea..d7e6e81 100644 --- a/bootstrapoptions_app_conversion.go +++ b/bootstrap/options_app_conversion.go @@ -1,13 +1,15 @@ -package europi +package bootstrap import ( "errors" "time" + + europi "github.com/awonak/EuroPiGo" ) // appHardwareWrapper sets up a wrapper around an app that expects a particular hardware interface // this is for automated parameter interpretation -func appHardwareWrapper[THardware Hardware](app any) any { +func appHardwareWrapper[THardware europi.Hardware](app any) any { start, _ := app.(ApplicationStart[THardware]) mainLoop, _ := app.(ApplicationMainLoop[THardware]) end, _ := app.(ApplicationEnd[THardware]) @@ -18,28 +20,29 @@ func appHardwareWrapper[THardware Hardware](app any) any { } } -func getAppFuncs(app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { - if appStart, _ := app.(ApplicationStart[Hardware]); appStart != nil { +func getAppFuncs(e europi.Hardware, app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { + if appStart, _ := app.(ApplicationStart[europi.Hardware]); appStart != nil { start = appStart.Start } - if appMainLoop, _ := app.(ApplicationMainLoop[Hardware]); appMainLoop != nil { + if appMainLoop, _ := app.(ApplicationMainLoop[europi.Hardware]); appMainLoop != nil { mainLoop = appMainLoop.MainLoop } - if appEnd, _ := app.(ApplicationEnd[Hardware]); appEnd != nil { + if appEnd, _ := app.(ApplicationEnd[europi.Hardware]); appEnd != nil { end = appEnd.End } - if start == nil && mainLoop == nil && end == nil { - start, mainLoop, end = getWrappedAppFuncs[*EuroPiPrototype](app) + switch e.(type) { + case *europi.EuroPiPrototype: + start, mainLoop, end = getWrappedAppFuncs[*europi.EuroPiPrototype](app) + case *europi.EuroPi: + start, mainLoop, end = getWrappedAppFuncs[*europi.EuroPi](app) + // TODO: add rev2 } - if start == nil && mainLoop == nil && end == nil { - start, mainLoop, end = getWrappedAppFuncs[*EuroPi](app) - } return } -func getWrappedAppFuncs[THardware Hardware](app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { +func getWrappedAppFuncs[THardware europi.Hardware](app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { appWrapper := appHardwareWrapper[THardware](app) if getStart, _ := appWrapper.(applicationStartProvider); getStart != nil { start = getStart.ApplicationStart() @@ -67,7 +70,7 @@ type applicationEndProvider interface { ApplicationEnd() AppEndFunc } -type appWrapper[THardware Hardware] struct { +type appWrapper[THardware europi.Hardware] struct { start ApplicationStart[THardware] mainLoop ApplicationMainLoop[THardware] end ApplicationEnd[THardware] @@ -80,9 +83,9 @@ func (a *appWrapper[THardware]) ApplicationStart() AppStartFunc { return a.doStart } -func (a *appWrapper[THardware]) doStart(e Hardware) { - pi, _ := e.(THardware) - if any(pi) == nil { +func (a *appWrapper[THardware]) doStart(e europi.Hardware) { + pi, ok := e.(THardware) + if !ok { panic(errors.New("incorrect hardware type conversion")) } a.start.Start(pi) @@ -95,9 +98,9 @@ func (a *appWrapper[THardware]) ApplicationMainLoop() AppMainLoopFunc { return a.doMainLoop } -func (a *appWrapper[THardware]) doMainLoop(e Hardware, deltaTime time.Duration) { - pi, _ := e.(THardware) - if any(pi) == nil { +func (a *appWrapper[THardware]) doMainLoop(e europi.Hardware, deltaTime time.Duration) { + pi, ok := e.(THardware) + if !ok { panic(errors.New("incorrect hardware type conversion")) } a.mainLoop.MainLoop(pi, deltaTime) @@ -110,9 +113,9 @@ func (a *appWrapper[THardware]) ApplicationEnd() AppEndFunc { return a.doEnd } -func (a *appWrapper[THardware]) doEnd(e Hardware) { - pi, _ := e.(THardware) - if any(pi) == nil { +func (a *appWrapper[THardware]) doEnd(e europi.Hardware) { + pi, ok := e.(THardware) + if !ok { panic(errors.New("incorrect hardware type conversion")) } a.end.End(pi) diff --git a/bootstrapoptions_features.go b/bootstrap/options_features.go similarity index 81% rename from bootstrapoptions_features.go rename to bootstrap/options_features.go index 7c350fe..ea6fc4b 100644 --- a/bootstrapoptions_features.go +++ b/bootstrap/options_features.go @@ -1,8 +1,4 @@ -package europi - -import ( - "errors" -) +package bootstrap const ( DefaultEnableDisplayLogger bool = false @@ -34,18 +30,6 @@ func InitRandom(enabled bool) BootstrapOption { } } -// UsingEuroPi sets a specific EuroPi object instance for all operations in the bootstrap -func UsingEuroPi(e Hardware) BootstrapOption { - return func(o *bootstrapConfig) error { - if e == nil { - return errors.New("europi instance must not be nil") - } - - o.europi = e - return nil - } -} - // AttachNonPicoWS (if enabled and on non-Pico builds with build flags of `-tags=revision1` set) // starts up a websocket interface and system debugger on port 8080 func AttachNonPicoWS(enabled bool) BootstrapOption { diff --git a/bootstrapoptions_lifecycle.go b/bootstrap/options_lifecycle.go similarity index 88% rename from bootstrapoptions_lifecycle.go rename to bootstrap/options_lifecycle.go index a03d7d7..52e8217 100644 --- a/bootstrapoptions_lifecycle.go +++ b/bootstrap/options_lifecycle.go @@ -1,8 +1,10 @@ -package europi +package bootstrap import ( "errors" "time" + + europi "github.com/awonak/EuroPiGo" ) /* Order of lifecycle calls: @@ -49,15 +51,15 @@ Bootstrap: destroyBootstrap */ type ( - PostBootstrapConstructionFunc func(e Hardware) - PreInitializeComponentsFunc func(e Hardware) - PostInitializeComponentsFunc func(e Hardware) - BootstrapCompletedFunc func(e Hardware) - AppStartFunc func(e Hardware) - AppMainLoopFunc func(e Hardware, deltaTime time.Duration) - AppEndFunc func(e Hardware) - BeginDestroyFunc func(e Hardware, reason any) - FinishDestroyFunc func(e Hardware) + PostBootstrapConstructionFunc func(e europi.Hardware) + PreInitializeComponentsFunc func(e europi.Hardware) + PostInitializeComponentsFunc func(e europi.Hardware) + BootstrapCompletedFunc func(e europi.Hardware) + AppStartFunc func(e europi.Hardware) + AppMainLoopFunc func(e europi.Hardware, deltaTime time.Duration) + AppEndFunc func(e europi.Hardware) + BeginDestroyFunc func(e europi.Hardware, reason any) + FinishDestroyFunc func(e europi.Hardware) ) // PostBootstrapConstruction sets the function that runs immediately after primary EuroPi bootstrap diff --git a/bootstrapoptions_ui.go b/bootstrap/options_ui.go similarity index 85% rename from bootstrapoptions_ui.go rename to bootstrap/options_ui.go index ce6da0c..3ae52cd 100644 --- a/bootstrapoptions_ui.go +++ b/bootstrap/options_ui.go @@ -1,12 +1,14 @@ -package europi +package bootstrap import ( "errors" "time" + + europi "github.com/awonak/EuroPiGo" ) // UI sets the user interface handler interface -func UI(ui UserInterface[Hardware], opts ...BootstrapUIOption) BootstrapOption { +func UI(ui UserInterface[europi.Hardware], opts ...BootstrapUIOption) BootstrapOption { return func(o *bootstrapConfig) error { if ui == nil { return errors.New("ui must not be nil") @@ -25,7 +27,7 @@ const ( type BootstrapUIOption func(o *bootstrapUIConfig) error type bootstrapUIConfig struct { - ui UserInterface[Hardware] + ui UserInterface[europi.Hardware] uiRefreshRate time.Duration options []BootstrapUIOption diff --git a/bootstrap_panic.go b/bootstrap/panic.go similarity index 75% rename from bootstrap_panic.go rename to bootstrap/panic.go index d82c6cb..3b11691 100644 --- a/bootstrap_panic.go +++ b/bootstrap/panic.go @@ -1,10 +1,11 @@ -package europi +package bootstrap import ( "fmt" "log" "os" + europi "github.com/awonak/EuroPiGo" "github.com/awonak/EuroPiGo/experimental/draw" "tinygo.org/x/tinydraw" ) @@ -12,14 +13,14 @@ import ( // DefaultPanicHandler is the default handler for panics // This will be set by the build flag `onscreenpanic` to `handlePanicOnScreenLog` // Not setting the build flag will set it to `handlePanicDisplayCrash` -var DefaultPanicHandler func(e Hardware, reason any) +var DefaultPanicHandler func(e europi.Hardware, reason any) var ( // silence linter _ = handlePanicOnScreenLog ) -func handlePanicOnScreenLog(e Hardware, reason any) { +func handlePanicOnScreenLog(e europi.Hardware, reason any) { if e == nil { // can't do anything if it's not enabled return @@ -36,12 +37,12 @@ func handlePanicOnScreenLog(e Hardware, reason any) { os.Exit(1) } -func handlePanicLogger(e Hardware, reason any) { +func handlePanicLogger(e europi.Hardware, reason any) { log.Panic(reason) } -func handlePanicDisplayCrash(e Hardware, reason any) { - display := Display(e) +func handlePanicDisplayCrash(e europi.Hardware, reason any) { + display := europi.Display(e) if display == nil { // can't do anything if we don't have a display return diff --git a/bootstrap_pico_panicdisabled.go b/bootstrap/pico_panicdisabled.go similarity index 95% rename from bootstrap_pico_panicdisabled.go rename to bootstrap/pico_panicdisabled.go index 0d5db92..ee43c61 100644 --- a/bootstrap_pico_panicdisabled.go +++ b/bootstrap/pico_panicdisabled.go @@ -1,7 +1,7 @@ //go:build pico && !onscreenpanic // +build pico,!onscreenpanic -package europi +package bootstrap import ( "github.com/awonak/EuroPiGo/hardware" diff --git a/bootstrap_pico_panicenabled.go b/bootstrap/pico_panicenabled.go similarity index 87% rename from bootstrap_pico_panicenabled.go rename to bootstrap/pico_panicenabled.go index 87a83ec..1b652ca 100644 --- a/bootstrap_pico_panicenabled.go +++ b/bootstrap/pico_panicenabled.go @@ -1,7 +1,7 @@ //go:build pico && onscreenpanic // +build pico,onscreenpanic -package europi +package bootstrap func init() { DefaultPanicHandler = handlePanicOnScreenLog diff --git a/bootstrap_ui.go b/bootstrap/ui.go similarity index 54% rename from bootstrap_ui.go rename to bootstrap/ui.go index 55be7e4..f5a9e5b 100644 --- a/bootstrap_ui.go +++ b/bootstrap/ui.go @@ -1,20 +1,22 @@ -package europi +package bootstrap import ( "context" "time" + + europi "github.com/awonak/EuroPiGo" ) -type UserInterface[THardware Hardware] interface { +type UserInterface[THardware europi.Hardware] interface { Start(e THardware) Paint(e THardware, deltaTime time.Duration) } -type UserInterfaceLogoPainter[THardware Hardware] interface { +type UserInterfaceLogoPainter[THardware europi.Hardware] interface { PaintLogo(e THardware, deltaTime time.Duration) } -type UserInterfaceButton1[THardware Hardware] interface { +type UserInterfaceButton1[THardware europi.Hardware] interface { Button1(e THardware, deltaTime time.Duration) } @@ -22,15 +24,15 @@ type UserInterfaceButton1Debounce interface { Button1Debounce() time.Duration } -type UserInterfaceButton1Ex[THardware Hardware] interface { +type UserInterfaceButton1Ex[THardware europi.Hardware] interface { Button1Ex(e THardware, value bool, deltaTime time.Duration) } -type UserInterfaceButton1Long[THardware Hardware] interface { +type UserInterfaceButton1Long[THardware europi.Hardware] interface { Button1Long(e THardware, deltaTime time.Duration) } -type UserInterfaceButton2[THardware Hardware] interface { +type UserInterfaceButton2[THardware europi.Hardware] interface { Button2(e THardware, deltaTime time.Duration) } @@ -38,11 +40,11 @@ type UserInterfaceButton2Debounce interface { Button2Debounce() time.Duration } -type UserInterfaceButton2Ex[THardware Hardware] interface { +type UserInterfaceButton2Ex[THardware europi.Hardware] interface { Button2Ex(e THardware, value bool, deltaTime time.Duration) } -type UserInterfaceButton2Long[THardware Hardware] interface { +type UserInterfaceButton2Long[THardware europi.Hardware] interface { Button2Long(e THardware, deltaTime time.Duration) } @@ -50,13 +52,13 @@ var ( ui uiModule ) -func enableUI(ctx context.Context, e Hardware, config bootstrapUIConfig) { +func enableUI(ctx context.Context, e europi.Hardware, config bootstrapUIConfig) { ui.setup(e, config.ui) ui.start(ctx, e, config.uiRefreshRate) } -func startUI(e Hardware) { +func startUI(e europi.Hardware) { if ui.screen == nil { return } @@ -65,10 +67,10 @@ func startUI(e Hardware) { } // ForceRepaintUI schedules a forced repaint of the UI (if it is configured and running) -func ForceRepaintUI(e Hardware) { +func ForceRepaintUI(e europi.Hardware) { ui.repaint() } -func disableUI(e Hardware) { +func disableUI(e europi.Hardware) { ui.shutdown() } diff --git a/bootstrap_uimodule.go b/bootstrap/uimodule.go similarity index 63% rename from bootstrap_uimodule.go rename to bootstrap/uimodule.go index e320576..16c6e6f 100644 --- a/bootstrap_uimodule.go +++ b/bootstrap/uimodule.go @@ -1,10 +1,11 @@ -package europi +package bootstrap import ( "context" "sync" "time" + europi "github.com/awonak/EuroPiGo" "github.com/awonak/EuroPiGo/debounce" "github.com/awonak/EuroPiGo/hardware/hal" ) @@ -15,31 +16,31 @@ import ( const LongPressDuration = time.Millisecond * 650 type uiModule struct { - screen UserInterface[Hardware] - logoPainter UserInterfaceLogoPainter[Hardware] + screen UserInterface[europi.Hardware] + logoPainter UserInterfaceLogoPainter[europi.Hardware] repaintCh chan struct{} stop context.CancelFunc wg sync.WaitGroup } -func (u *uiModule) setup(e Hardware, screen UserInterface[Hardware]) { - b1 := Button(e, 0) - b2 := Button(e, 1) +func (u *uiModule) setup(e europi.Hardware, screen UserInterface[europi.Hardware]) { + b1 := europi.Button(e, 0) + b2 := europi.Button(e, 1) ui.screen = screen if ui.screen == nil { return } - ui.logoPainter, _ = screen.(UserInterfaceLogoPainter[Hardware]) + ui.logoPainter, _ = screen.(UserInterfaceLogoPainter[europi.Hardware]) ui.repaintCh = make(chan struct{}, 1) var ( - inputB1 func(e Hardware, value bool, deltaTime time.Duration) - inputB1L func(e Hardware, deltaTime time.Duration) + inputB1 func(e europi.Hardware, value bool, deltaTime time.Duration) + inputB1L func(e europi.Hardware, deltaTime time.Duration) ) - if in, ok := screen.(UserInterfaceButton1[Hardware]); ok { + if in, ok := screen.(UserInterfaceButton1[europi.Hardware]); ok { var debounceDelay time.Duration if db, ok := screen.(UserInterfaceButton1Debounce); ok { debounceDelay = db.Button1Debounce() @@ -49,22 +50,22 @@ func (u *uiModule) setup(e Hardware, screen UserInterface[Hardware]) { in.Button1(e, deltaTime) } }).Debounce(debounceDelay) - inputB1 = func(e Hardware, value bool, deltaTime time.Duration) { + inputB1 = func(e europi.Hardware, value bool, deltaTime time.Duration) { inputDB(value) } - } else if in, ok := screen.(UserInterfaceButton1Ex[Hardware]); ok { + } else if in, ok := screen.(UserInterfaceButton1Ex[europi.Hardware]); ok { inputB1 = in.Button1Ex } - if in, ok := screen.(UserInterfaceButton1Long[Hardware]); ok { + if in, ok := screen.(UserInterfaceButton1Long[europi.Hardware]); ok { inputB1L = in.Button1Long } ui.setupButton(e, b1, inputB1, inputB1L) var ( - inputB2 func(e Hardware, value bool, deltaTime time.Duration) - inputB2L func(e Hardware, deltaTime time.Duration) + inputB2 func(e europi.Hardware, value bool, deltaTime time.Duration) + inputB2L func(e europi.Hardware, deltaTime time.Duration) ) - if in, ok := screen.(UserInterfaceButton2[Hardware]); ok { + if in, ok := screen.(UserInterfaceButton2[europi.Hardware]); ok { var debounceDelay time.Duration if db, ok := screen.(UserInterfaceButton2Debounce); ok { debounceDelay = db.Button2Debounce() @@ -74,19 +75,19 @@ func (u *uiModule) setup(e Hardware, screen UserInterface[Hardware]) { in.Button2(e, deltaTime) } }).Debounce(debounceDelay) - inputB2 = func(e Hardware, value bool, deltaTime time.Duration) { + inputB2 = func(e europi.Hardware, value bool, deltaTime time.Duration) { inputDB(value) } - } else if in, ok := screen.(UserInterfaceButton2Ex[Hardware]); ok { + } else if in, ok := screen.(UserInterfaceButton2Ex[europi.Hardware]); ok { inputB2 = in.Button2Ex } - if in, ok := screen.(UserInterfaceButton2Long[Hardware]); ok { + if in, ok := screen.(UserInterfaceButton2Long[europi.Hardware]); ok { inputB2L = in.Button2Long } ui.setupButton(e, b2, inputB2, inputB2L) } -func (u *uiModule) start(ctx context.Context, e Hardware, interval time.Duration) { +func (u *uiModule) start(ctx context.Context, e europi.Hardware, interval time.Duration) { ui.wg.Add(1) go ui.run(ctx, e, interval) } @@ -113,10 +114,10 @@ func (u *uiModule) shutdown() { ui.wait() } -func (u *uiModule) run(ctx context.Context, e Hardware, interval time.Duration) { +func (u *uiModule) run(ctx context.Context, e europi.Hardware, interval time.Duration) { defer u.wg.Done() - disp := Display(e) + disp := europi.Display(e) if disp == nil { // no display means no ui // TODO: make uiModule work when any user input/output is specified, not just display @@ -159,7 +160,7 @@ func (u *uiModule) run(ctx context.Context, e Hardware, interval time.Duration) } } -func (u *uiModule) setupButton(e Hardware, btn hal.ButtonInput, onShort func(e Hardware, value bool, deltaTime time.Duration), onLong func(e Hardware, deltaTime time.Duration)) { +func (u *uiModule) setupButton(e europi.Hardware, btn hal.ButtonInput, onShort func(e europi.Hardware, value bool, deltaTime time.Duration), onLong func(e europi.Hardware, deltaTime time.Duration)) { if btn == nil { return } @@ -170,12 +171,12 @@ func (u *uiModule) setupButton(e Hardware, btn hal.ButtonInput, onShort func(e H if onShort == nil { // no-op - onShort = func(e Hardware, value bool, deltaTime time.Duration) {} + onShort = func(e europi.Hardware, value bool, deltaTime time.Duration) {} } // if no long-press handler present, just reuse short-press handler if onLong == nil { - onLong = func(e Hardware, deltaTime time.Duration) { + onLong = func(e europi.Hardware, deltaTime time.Duration) { onShort(e, false, deltaTime) } } diff --git a/europi.go b/europi.go index 2dcfd52..f98b8b2 100644 --- a/europi.go +++ b/europi.go @@ -20,6 +20,9 @@ type ( // 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) diff --git a/europi_nonpico.go b/europi_nonpico.go new file mode 100644 index 0000000..4e3933f --- /dev/null +++ b/europi_nonpico.go @@ -0,0 +1,18 @@ +//go:build !pico +// +build !pico + +package europi + +import ( + "github.com/awonak/EuroPiGo/hardware" + "github.com/awonak/EuroPiGo/internal/nonpico" +) + +func nonPicoEnsureHardware() { + 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..657ae1f --- /dev/null +++ b/europi_pico.go @@ -0,0 +1,18 @@ +//go:build pico +// +build pico + +package europi + +import ( + "github.com/awonak/EuroPiGo/hardware" + "github.com/awonak/EuroPiGo/internal/pico" +) + +func picoEnsureHardware() { + rev := pico.DetectRevision() + hardware.SetDetectedRevision(rev) +} + +func init() { + ensureHardware = picoEnsureHardware +} diff --git a/experimental/screenbank/screenbank.go b/experimental/screenbank/screenbank.go index f647bb3..07227bc 100644 --- a/experimental/screenbank/screenbank.go +++ b/experimental/screenbank/screenbank.go @@ -46,7 +46,7 @@ func (sb *ScreenBank) Current() *screenBankEntryDetails { if len(sb.bank) == 0 { return nil } - return sb.bank[sb.current].screen + return &sb.bank[sb.current].screen } func (sb *ScreenBank) transitionTo(idx int) { @@ -85,7 +85,7 @@ func (sb *ScreenBank) Start(e europi.Hardware) { s := &sb.bank[i] s.lock() - s.screen.Start(e) + s.screen.screen.Start(e) s.lastUpdate = time.Now() s.unlock() } @@ -114,25 +114,25 @@ func (sb *ScreenBank) Paint(e europi.Hardware, deltaTime time.Duration) { cur := &sb.bank[sb.current] cur.lock() now := time.Now() - cur.screen.Paint(e, now.Sub(cur.lastUpdate)) + cur.screen.screen.Paint(e, now.Sub(cur.lastUpdate)) cur.lastUpdate = now cur.unlock() } func (sb *ScreenBank) Button1Ex(e europi.Hardware, value bool, deltaTime time.Duration) { screen := sb.Current() - if cur := screen.UserInterfaceButton1; cur != nil { + if cur := screen.button1; cur != nil { if !value { cur.Button1(e, deltaTime) } - } else if cur := screen.UserInterfaceButton1Ex; cur != nil { + } else if cur := screen.button1Ex; cur != nil { cur.Button1Ex(e, value, deltaTime) } } func (sb *ScreenBank) Button1Long(e europi.Hardware, deltaTime time.Duration) { screen := sb.Current() - if cur := screen.UserInterfaceButton1Long; cur != nil { + if cur := screen.button1Long; cur != nil { cur.Button1Long(e, deltaTime) } else { // try the short-press @@ -142,11 +142,11 @@ func (sb *ScreenBank) Button1Long(e europi.Hardware, deltaTime time.Duration) { func (sb *ScreenBank) Button2Ex(e europi.Hardware, value bool, deltaTime time.Duration) { screen := sb.Current() - if cur := screen.UserInterfaceButton2; cur != nil { + if cur := screen.button2; cur != nil { if !value { cur.Button2(e, deltaTime) } - } else if cur := screen.UserInterfaceButton2Ex; cur != nil { + } else if cur := screen.button2Ex; cur != nil { cur.Button2Ex(e, value, deltaTime) } } diff --git a/experimental/screenbank/screenbankentry.go b/experimental/screenbank/screenbankentry.go index deda8a4..9812a9f 100644 --- a/experimental/screenbank/screenbankentry.go +++ b/experimental/screenbank/screenbankentry.go @@ -4,12 +4,13 @@ import ( "time" europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/bootstrap" ) type screenBankEntry struct { name string logo string - screen *screenBankEntryDetails + screen screenBankEntryDetails enabled bool locked bool lastUpdate time.Time @@ -32,10 +33,10 @@ func (e *screenBankEntry) unlock() { } type screenBankEntryDetails struct { - europi.UserInterface[europi.Hardware] - europi.UserInterfaceButton1[europi.Hardware] - europi.UserInterfaceButton1Long[europi.Hardware] - europi.UserInterfaceButton1Ex[europi.Hardware] - europi.UserInterfaceButton2[europi.Hardware] - europi.UserInterfaceButton2Ex[europi.Hardware] + screen bootstrap.UserInterface[europi.Hardware] + button1 bootstrap.UserInterfaceButton1[europi.Hardware] + button1Long bootstrap.UserInterfaceButton1Long[europi.Hardware] + button1Ex bootstrap.UserInterfaceButton1Ex[europi.Hardware] + button2 bootstrap.UserInterfaceButton2[europi.Hardware] + button2Ex bootstrap.UserInterfaceButton2Ex[europi.Hardware] } diff --git a/experimental/screenbank/screenbankoptions.go b/experimental/screenbank/screenbankoptions.go index 8e7fb19..8b574c7 100644 --- a/experimental/screenbank/screenbankoptions.go +++ b/experimental/screenbank/screenbankoptions.go @@ -5,6 +5,7 @@ import ( "time" europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/bootstrap" ) type ScreenBankOption func(sb *ScreenBank) error @@ -13,9 +14,9 @@ type ScreenBankOption func(sb *ScreenBank) error // logo is the emoji to use (see https://github.com/tinygo-org/tinyfont/blob/release/notoemoji/NotoEmoji-Regular-12pt.go) func WithScreen(name string, logo string, screen any) ScreenBankOption { return func(sb *ScreenBank) error { - details := getScreen(screen) - if details == nil { - return fmt.Errorf("screen %q does not implement a variant of europi.UserInterface", name) + details, ok := getScreen(screen) + if !ok { + return fmt.Errorf("screen %q does not implement a variant of bootstrap.UserInterface", name) } e := screenBankEntry{ name: name, @@ -31,111 +32,125 @@ func WithScreen(name string, logo string, screen any) ScreenBankOption { } } -func getScreen(screen any) *screenBankEntryDetails { - if s, _ := screen.(europi.UserInterface[europi.Hardware]); s != nil { - details := &screenBankEntryDetails{ - UserInterface: s, - } - - details.UserInterfaceButton1, _ = screen.(europi.UserInterfaceButton1[europi.Hardware]) - details.UserInterfaceButton1Long, _ = screen.(europi.UserInterfaceButton1Long[europi.Hardware]) - details.UserInterfaceButton1Ex, _ = screen.(europi.UserInterfaceButton1Ex[europi.Hardware]) - details.UserInterfaceButton2, _ = screen.(europi.UserInterfaceButton2[europi.Hardware]) - details.UserInterfaceButton2Ex, _ = screen.(europi.UserInterfaceButton2Ex[europi.Hardware]) - - return details +func getScreen(screen any) (details screenBankEntryDetails, ok bool) { + if s, _ := screen.(bootstrap.UserInterface[europi.Hardware]); s != nil { + details.screen = s + details.button1, _ = screen.(bootstrap.UserInterfaceButton1[europi.Hardware]) + details.button1Long, _ = screen.(bootstrap.UserInterfaceButton1Long[europi.Hardware]) + details.button1Ex, _ = screen.(bootstrap.UserInterfaceButton1Ex[europi.Hardware]) + details.button2, _ = screen.(bootstrap.UserInterfaceButton2[europi.Hardware]) + details.button2Ex, _ = screen.(bootstrap.UserInterfaceButton2Ex[europi.Hardware]) + + ok = true + return } - if s := getScreenForHardware[*europi.EuroPiPrototype](screen); s != nil { - return s + if details, ok = getScreenForHardware[*europi.EuroPiPrototype](screen); ok { + return } - if s := getScreenForHardware[*europi.EuroPi](screen); s != nil { - return s + if details, ok = getScreenForHardware[*europi.EuroPi](screen); ok { + return } // TODO: add rev2 - return nil + return } -func getScreenForHardware[THardware europi.Hardware](screen any) *screenBankEntryDetails { - s, _ := screen.(europi.UserInterface[THardware]) +func getScreenForHardware[THardware europi.Hardware](screen any) (details screenBankEntryDetails, ok bool) { + s, _ := screen.(bootstrap.UserInterface[THardware]) if s == nil { - return nil + return } wrapper := &screenHardwareWrapper[THardware]{ - UserInterface: s, + screen: s, } - details := &screenBankEntryDetails{ - UserInterface: wrapper, - } + details.screen = wrapper - if wrapper.button1, _ = screen.(europi.UserInterfaceButton1[THardware]); wrapper.button1 != nil { - details.UserInterfaceButton1 = wrapper + if wrapper.button1, _ = screen.(bootstrap.UserInterfaceButton1[THardware]); wrapper.button1 != nil { + details.button1 = wrapper } - if wrapper.button1Long, _ = screen.(europi.UserInterfaceButton1Long[THardware]); wrapper.button1Long != nil { - details.UserInterfaceButton1Long = wrapper + if wrapper.button1Long, _ = screen.(bootstrap.UserInterfaceButton1Long[THardware]); wrapper.button1Long != nil { + details.button1Long = wrapper } - if wrapper.button1Ex, _ = screen.(europi.UserInterfaceButton1Ex[THardware]); wrapper.button1Ex != nil { - details.UserInterfaceButton1Ex = wrapper + if wrapper.button1Ex, _ = screen.(bootstrap.UserInterfaceButton1Ex[THardware]); wrapper.button1Ex != nil { + details.button1Ex = wrapper } - if wrapper.button2, _ = screen.(europi.UserInterfaceButton2[THardware]); wrapper.button2 != nil { - details.UserInterfaceButton2 = wrapper + if wrapper.button2, _ = screen.(bootstrap.UserInterfaceButton2[THardware]); wrapper.button2 != nil { + details.button2 = wrapper } - if wrapper.button2Ex, _ = screen.(europi.UserInterfaceButton2Ex[THardware]); wrapper.button2Ex != nil { - details.UserInterfaceButton2Ex = wrapper + if wrapper.button2Ex, _ = screen.(bootstrap.UserInterfaceButton2Ex[THardware]); wrapper.button2Ex != nil { + details.button2Ex = wrapper } - return details + ok = true + return } type screenHardwareWrapper[THardware europi.Hardware] struct { - europi.UserInterface[THardware] - button1 europi.UserInterfaceButton1[THardware] - button1Ex europi.UserInterfaceButton1Ex[THardware] - button1Long europi.UserInterfaceButton1Long[THardware] - button2 europi.UserInterfaceButton2[THardware] - button2Ex europi.UserInterfaceButton2Ex[THardware] + screen bootstrap.UserInterface[THardware] + button1 bootstrap.UserInterfaceButton1[THardware] + button1Ex bootstrap.UserInterfaceButton1Ex[THardware] + button1Long bootstrap.UserInterfaceButton1Long[THardware] + button2 bootstrap.UserInterfaceButton2[THardware] + button2Ex bootstrap.UserInterfaceButton2Ex[THardware] } func (w *screenHardwareWrapper[THardware]) Start(e europi.Hardware) { - pi, _ := e.(THardware) - w.UserInterface.Start(pi) + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.screen.Start(pi) } func (w *screenHardwareWrapper[THardware]) Paint(e europi.Hardware, deltaTime time.Duration) { - pi, _ := e.(THardware) - w.UserInterface.Paint(pi, deltaTime) + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.screen.Paint(pi, deltaTime) } func (w *screenHardwareWrapper[THardware]) Button1(e europi.Hardware, deltaTime time.Duration) { - pi, _ := e.(THardware) + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } w.button1.Button1(pi, deltaTime) } func (w *screenHardwareWrapper[THardware]) Button1Ex(e europi.Hardware, value bool, deltaTime time.Duration) { - pi, _ := e.(THardware) + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } w.button1Ex.Button1Ex(pi, value, deltaTime) } func (w *screenHardwareWrapper[THardware]) Button1Long(e europi.Hardware, deltaTime time.Duration) { - pi, _ := e.(THardware) + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } w.button1Long.Button1Long(pi, deltaTime) } func (w *screenHardwareWrapper[THardware]) Button2(e europi.Hardware, deltaTime time.Duration) { - pi, _ := e.(THardware) + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } w.button2.Button2(pi, deltaTime) } func (w *screenHardwareWrapper[THardware]) Button2Ex(e europi.Hardware, value bool, deltaTime time.Duration) { - pi, _ := e.(THardware) + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } w.button2Ex.Button2Ex(pi, value, deltaTime) } - -// Button1Debounce() time.Duration -// Button2Debounce() time.Duration -// Button2Long(e THardware, deltaTime time.Duration) diff --git a/hardware/common/contextpi.go b/hardware/common/contextpi.go new file mode 100644 index 0000000..2cb4845 --- /dev/null +++ b/hardware/common/contextpi.go @@ -0,0 +1,23 @@ +package common + +import "time" + +// ContextPi gives the EuroPi hardware the components necessary +// to perform rudimentary context operations +type ContextPi struct{} + +func (ContextPi) Deadline() (deadline time.Time, ok bool) { + return +} + +func (ContextPi) Done() <-chan struct{} { + return nil +} + +func (ContextPi) Err() error { + return nil +} + +func (ContextPi) Value(key any) any { + return nil +} diff --git a/hardware/hal/hardware.go b/hardware/hal/hardware.go index ca91bfa..71fd1ec 100644 --- a/hardware/hal/hardware.go +++ b/hardware/hal/hardware.go @@ -1,5 +1,7 @@ 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 @@ -32,6 +34,7 @@ const ( // Hardware is the collection of component wrappers used to interact with the module. type Hardware interface { + Context() context.Context Revision() Revision Random() RandomGenerator Button(idx int) ButtonInput diff --git a/hardware/platform.go b/hardware/platform.go index 653e588..5610eef 100644 --- a/hardware/platform.go +++ b/hardware/platform.go @@ -30,34 +30,8 @@ func GetHardware[T any](revision hal.Revision, id hal.HardwareId) T { } } -var ( - onRevisionDetected = make(chan func(revision hal.Revision), 10) - OnRevisionDetected chan<- func(revision hal.Revision) = onRevisionDetected - revisionWgDone sync.Once - hardwareReady atomic.Value - hardwareReadyMu sync.Mutex - hardwareReadyCond = sync.NewCond(&hardwareReadyMu) -) - -func SetDetectedRevision(opts ...hal.Revision) { - // 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()) - } - } - }() - }) -} - -func SetReady() { - hardwareReady.Store(true) - hardwareReadyCond.Broadcast() -} - +// WaitForReady awaits the readiness of the hardware initialization. +// This will block until every aspect of hardware initialization has completed. func WaitForReady() { hardwareReadyCond.L.Lock() for { @@ -70,14 +44,15 @@ func WaitForReady() { hardwareReadyCond.L.Unlock() } -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 ( + hardwareReady atomic.Value + hardwareReadyMu sync.Mutex + hardwareReadyCond = sync.NewCond(&hardwareReadyMu) +) + +// SetReady is used by the hardware initialization code. +// Do not call this function directly. +func SetReady() { + hardwareReady.Store(true) + hardwareReadyCond.Broadcast() } diff --git a/hardware/rev0/platform.go b/hardware/rev0/platform.go index 1d58dc8..0b0419a 100644 --- a/hardware/rev0/platform.go +++ b/hardware/rev0/platform.go @@ -1,6 +1,8 @@ package rev0 import ( + "context" + "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" ) @@ -15,6 +17,8 @@ var ( ) 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 @@ -43,6 +47,10 @@ type EuroPiPrototype struct { RND hal.RandomGenerator } +func (e *EuroPiPrototype) Context() context.Context { + return e +} + func (e *EuroPiPrototype) Revision() hal.Revision { return hal.Revision0 } @@ -51,6 +59,10 @@ 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} } diff --git a/hardware/rev0/voltageoutput.go b/hardware/rev0/voltageoutput.go index 978c2d1..8577370 100644 --- a/hardware/rev0/voltageoutput.go +++ b/hardware/rev0/voltageoutput.go @@ -16,11 +16,11 @@ const ( 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. -var DefaultPWMPeriod time.Duration = time.Nanosecond * 500 + // 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 ( cvInitialConfig = hal.VoltageOutputConfig{ diff --git a/hardware/rev1/platform.go b/hardware/rev1/platform.go index 36dff85..2ab6f2a 100644 --- a/hardware/rev1/platform.go +++ b/hardware/rev1/platform.go @@ -1,6 +1,8 @@ package rev1 import ( + "context" + "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" ) @@ -10,6 +12,8 @@ import ( 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 @@ -40,6 +44,10 @@ type EuroPi struct { RND hal.RandomGenerator } +func (e *EuroPi) Context() context.Context { + return e +} + func (e *EuroPi) Revision() hal.Revision { return hal.Revision1 } @@ -48,6 +56,10 @@ 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} } diff --git a/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go index 2548165..77caf53 100644 --- a/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -16,11 +16,11 @@ const ( 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. -var DefaultPWMPeriod time.Duration = time.Nanosecond * 500 + // 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 ( cvInitialConfig = hal.VoltageOutputConfig{ diff --git a/hardware/revisiondetection.go b/hardware/revisiondetection.go new file mode 100644 index 0000000..eb866a1 --- /dev/null +++ b/hardware/revisiondetection.go @@ -0,0 +1,42 @@ +package hardware + +import ( + "sync" + + "github.com/awonak/EuroPiGo/hardware/hal" +) + +// GetRevision returns the currently detected hardware revision. +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 = make(chan func(revision hal.Revision), 10) + OnRevisionDetected chan<- func(revision hal.Revision) = onRevisionDetected + revisionWgDone sync.Once +) + +// SetDetectedRevision sets the currently detected hardware revision. +// This should not be called directly. +func SetDetectedRevision(opts ...hal.Revision) { + // 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/nonpico.go b/internal/nonpico/nonpico.go index bf8b0d5..1a0d31f 100644 --- a/internal/nonpico/nonpico.go +++ b/internal/nonpico/nonpico.go @@ -22,6 +22,12 @@ func initRevision2() { //TODO: rev2.DoInit() } +var detectedRevision hal.Revision + +func DetectRevision() hal.Revision { + return detectedRevision +} + func init() { hardware.OnRevisionDetected <- func(revision hal.Revision) { switch revision { diff --git a/internal/nonpico/rev0.go b/internal/nonpico/rev0.go index 357e53d..aac032e 100644 --- a/internal/nonpico/rev0.go +++ b/internal/nonpico/rev0.go @@ -5,10 +5,9 @@ package nonpico import ( - "github.com/awonak/EuroPiGo/hardware" "github.com/awonak/EuroPiGo/hardware/hal" ) func init() { - hardware.SetDetectedRevision(hal.Revision0) + detectedRevision = hal.Revision0 } diff --git a/internal/nonpico/rev1.go b/internal/nonpico/rev1.go index a868e0f..9aaeeb2 100644 --- a/internal/nonpico/rev1.go +++ b/internal/nonpico/rev1.go @@ -5,10 +5,9 @@ package nonpico import ( - "github.com/awonak/EuroPiGo/hardware" "github.com/awonak/EuroPiGo/hardware/hal" ) func init() { - hardware.SetDetectedRevision(hal.Revision1) + detectedRevision = hal.Revision1 } diff --git a/internal/nonpico/rev2.go b/internal/nonpico/rev2.go index b0a2129..64d4f2e 100644 --- a/internal/nonpico/rev2.go +++ b/internal/nonpico/rev2.go @@ -5,10 +5,9 @@ package nonpico import ( - "github.com/awonak/EuroPiGo/hardware" "github.com/awonak/EuroPiGo/hardware/hal" ) func init() { - hardware.SetDetectedRevision(hal.Revision2) + detectedRevision = hal.Revision2 } diff --git a/internal/nonpico/ws/websocket.go b/internal/nonpico/ws/websocket.go index 9f85648..69105df 100644 --- a/internal/nonpico/ws/websocket.go +++ b/internal/nonpico/ws/websocket.go @@ -83,7 +83,7 @@ func Upgrade(w http.ResponseWriter, r *http.Request) (*WebSocket, error) { return nil, err } - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(r.Context()) conn.SetReadLimit(2048) _ = conn.SetWriteDeadline(time.Now().Add(pongWait)) diff --git a/internal/pico/revisiondetection.go b/internal/pico/revisiondetection.go index 8a5550d..f5741e4 100644 --- a/internal/pico/revisiondetection.go +++ b/internal/pico/revisiondetection.go @@ -7,7 +7,6 @@ import ( "machine" "runtime/interrupt" - "github.com/awonak/EuroPiGo/hardware" "github.com/awonak/EuroPiGo/hardware/hal" ) @@ -42,6 +41,9 @@ 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 @@ -50,7 +52,4 @@ func DetectRevision() hal.Revision { } } -func init() { - rev := DetectRevision() - hardware.SetDetectedRevision(rev) -} +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/projects/clockgenerator/clockgenerator.go b/internal/projects/clockgenerator/clockgenerator.go index 4809c49..d8b5a41 100644 --- a/internal/projects/clockgenerator/clockgenerator.go +++ b/internal/projects/clockgenerator/clockgenerator.go @@ -4,6 +4,7 @@ import ( "time" europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/bootstrap" "github.com/awonak/EuroPiGo/experimental/screenbank" "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" @@ -53,7 +54,7 @@ func (app *application) Start(e *europi.EuroPi) { } else { e.CV1.SetCV(0.0) } - europi.ForceRepaintUI(e) + bootstrap.ForceRepaintUI(e) }, }); err != nil { panic(err) @@ -70,18 +71,21 @@ func main() { panic(err) } + pi := europi.New() + // some options shown below are being explicitly set to their defaults // only to showcase their existence. - if err := europi.Bootstrap( - europi.EnableDisplayLogger(false), - europi.InitRandom(true), - europi.App( + if err := bootstrap.Bootstrap( + pi, + bootstrap.EnableDisplayLogger(false), + bootstrap.InitRandom(true), + bootstrap.App( app, - europi.AppMainLoopInterval(time.Millisecond*1), + bootstrap.AppMainLoopInterval(time.Millisecond*1), ), - europi.UI( + bootstrap.UI( app.ui, - europi.UIRefreshRate(time.Millisecond*50), + bootstrap.UIRefreshRate(time.Millisecond*50), ), ); err != nil { panic(err) diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index 426cc9e..e33ce0a 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -35,6 +35,7 @@ import ( "tinygo.org/x/tinyfont/proggy" europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/bootstrap" "github.com/awonak/EuroPiGo/clamp" "github.com/awonak/EuroPiGo/experimental/draw" "github.com/awonak/EuroPiGo/experimental/fontwriter" @@ -293,6 +294,13 @@ func main() { 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 := bootstrap.ActivateNonPicoWS(e.Context(), e); ws != nil { + defer func() { + _ = ws.Shutdown() + }() + } + appStart(e) // Check for clock updates every 2 seconds. diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index e32f047..f19167a 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -9,6 +9,7 @@ import ( "tinygo.org/x/tinyfont/proggy" europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/bootstrap" "github.com/awonak/EuroPiGo/experimental/draw" "github.com/awonak/EuroPiGo/experimental/fontwriter" ) @@ -93,6 +94,13 @@ func main() { 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 := bootstrap.ActivateNonPicoWS(e.Context(), e); ws != nil { + defer func() { + _ = ws.Shutdown() + }() + } + appStart(e) for { mainLoop(e) diff --git a/internal/projects/randomskips/randomskips.go b/internal/projects/randomskips/randomskips.go index fd50daf..dc2cc50 100644 --- a/internal/projects/randomskips/randomskips.go +++ b/internal/projects/randomskips/randomskips.go @@ -4,6 +4,7 @@ import ( "time" europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/bootstrap" "github.com/awonak/EuroPiGo/experimental/screenbank" "github.com/awonak/EuroPiGo/hardware/hal" clockgenerator "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" @@ -96,18 +97,21 @@ func main() { panic(err) } + pi := europi.New() + // some options shown below are being explicitly set to their defaults // only to showcase their existence. - if err := europi.Bootstrap( - europi.EnableDisplayLogger(false), - europi.InitRandom(true), - europi.App( + if err := bootstrap.Bootstrap( + pi, + bootstrap.EnableDisplayLogger(false), + bootstrap.InitRandom(true), + bootstrap.App( app, - europi.AppMainLoopInterval(time.Millisecond*1), + bootstrap.AppMainLoopInterval(time.Millisecond*1), ), - europi.UI( + bootstrap.UI( app.ui, - europi.UIRefreshRate(time.Millisecond*50), + bootstrap.UIRefreshRate(time.Millisecond*50), ), ); err != nil { panic(err) diff --git a/revisiondetection.go b/revisiondetection.go new file mode 100644 index 0000000..035816e --- /dev/null +++ b/revisiondetection.go @@ -0,0 +1,7 @@ +package europi + +var ensureHardware func() + +func EnsureHardware() { + ensureHardware() +} From bcb574277cfa2bb326f475a0c80bcf3fbaf1333b Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 5 May 2023 15:25:33 -0700 Subject: [PATCH 52/62] simplify init slightly - reduces chance of races --- bootstrap/panic.go | 2 +- bootstrap/pico_panicdisabled.go | 2 +- hardware/platform.go | 11 ++++++++++- hardware/revisiondetection.go | 22 ++++++++++++++++++---- internal/nonpico/nonpico.go | 2 +- internal/pico/pico.go | 2 +- 6 files changed, 32 insertions(+), 9 deletions(-) diff --git a/bootstrap/panic.go b/bootstrap/panic.go index 3b11691..6e65d52 100644 --- a/bootstrap/panic.go +++ b/bootstrap/panic.go @@ -17,7 +17,7 @@ var DefaultPanicHandler func(e europi.Hardware, reason any) var ( // silence linter - _ = handlePanicOnScreenLog + _, _ = handlePanicOnScreenLog, handlePanicDisplayCrash ) func handlePanicOnScreenLog(e europi.Hardware, reason any) { diff --git a/bootstrap/pico_panicdisabled.go b/bootstrap/pico_panicdisabled.go index ee43c61..eefb6df 100644 --- a/bootstrap/pico_panicdisabled.go +++ b/bootstrap/pico_panicdisabled.go @@ -9,7 +9,7 @@ import ( ) func init() { - hardware.OnRevisionDetected <- func(revision hal.Revision) { + hardware.OnRevisionDetected() <- func(revision hal.Revision) { switch revision { case hal.RevisionUnknown, hal.EuroPiProto: DefaultPanicHandler = handlePanicLogger diff --git a/hardware/platform.go b/hardware/platform.go index 5610eef..515a5cf 100644 --- a/hardware/platform.go +++ b/hardware/platform.go @@ -33,6 +33,7 @@ func GetHardware[T any](revision hal.Revision, id hal.HardwareId) T { // 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() @@ -47,12 +48,20 @@ func WaitForReady() { var ( hardwareReady atomic.Value hardwareReadyMu sync.Mutex - hardwareReadyCond = sync.NewCond(&hardwareReadyMu) + 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/revisiondetection.go b/hardware/revisiondetection.go index eb866a1..f8c01fb 100644 --- a/hardware/revisiondetection.go +++ b/hardware/revisiondetection.go @@ -7,11 +7,13 @@ import ( ) // 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) { + OnRevisionDetected() <- func(revision hal.Revision) { detectedRevision = revision waitForDetect.Done() } @@ -20,14 +22,26 @@ func GetRevision() hal.Revision { } var ( - onRevisionDetected = make(chan func(revision hal.Revision), 10) - OnRevisionDetected chan<- func(revision hal.Revision) = onRevisionDetected - revisionWgDone sync.Once + 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() chan<- func(revision hal.Revision) { + ensureOnRevisionDetection() + return onRevisionDetected +} + // 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() { diff --git a/internal/nonpico/nonpico.go b/internal/nonpico/nonpico.go index 1a0d31f..9dfe85c 100644 --- a/internal/nonpico/nonpico.go +++ b/internal/nonpico/nonpico.go @@ -29,7 +29,7 @@ func DetectRevision() hal.Revision { } func init() { - hardware.OnRevisionDetected <- func(revision hal.Revision) { + hardware.OnRevisionDetected() <- func(revision hal.Revision) { switch revision { case hal.Revision0: initRevision0() diff --git a/internal/pico/pico.go b/internal/pico/pico.go index 3318c49..38943fd 100644 --- a/internal/pico/pico.go +++ b/internal/pico/pico.go @@ -57,7 +57,7 @@ func initRevision2() { } func init() { - hardware.OnRevisionDetected <- func(revision hal.Revision) { + hardware.OnRevisionDetected() <- func(revision hal.Revision) { switch revision { case hal.Revision0: initRevision0() From 6b49c574754633a3a234244e1d2992b24f5fdc32 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 5 May 2023 15:34:02 -0700 Subject: [PATCH 53/62] simplify screenbank --- experimental/screenbank/entry.go | 32 ++++++++ experimental/screenbank/entrywrapper.go | 73 +++++++++++++++++++ .../{screenbankoptions.go => options.go} | 73 +------------------ experimental/screenbank/screenbank.go | 4 +- experimental/screenbank/screenbankentry.go | 42 ----------- 5 files changed, 111 insertions(+), 113 deletions(-) create mode 100644 experimental/screenbank/entry.go create mode 100644 experimental/screenbank/entrywrapper.go rename experimental/screenbank/{screenbankoptions.go => options.go} (53%) delete mode 100644 experimental/screenbank/screenbankentry.go diff --git a/experimental/screenbank/entry.go b/experimental/screenbank/entry.go new file mode 100644 index 0000000..c65e2f2 --- /dev/null +++ b/experimental/screenbank/entry.go @@ -0,0 +1,32 @@ +package screenbank + +import ( + "time" + + europi "github.com/awonak/EuroPiGo" +) + +type entry struct { + name string + logo string + screen entryWrapper[europi.Hardware] + enabled bool + locked bool + lastUpdate time.Time +} + +func (e *entry) lock() { + if e.locked { + return + } + + e.locked = true +} + +func (e *entry) unlock() { + if !e.enabled { + return + } + + e.locked = false +} diff --git a/experimental/screenbank/entrywrapper.go b/experimental/screenbank/entrywrapper.go new file mode 100644 index 0000000..dbe3f06 --- /dev/null +++ b/experimental/screenbank/entrywrapper.go @@ -0,0 +1,73 @@ +package screenbank + +import ( + "time" + + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/bootstrap" +) + +type entryWrapper[THardware europi.Hardware] struct { + screen bootstrap.UserInterface[THardware] + button1 bootstrap.UserInterfaceButton1[THardware] + button1Long bootstrap.UserInterfaceButton1Long[THardware] + button1Ex bootstrap.UserInterfaceButton1Ex[THardware] + button2 bootstrap.UserInterfaceButton2[THardware] + button2Ex bootstrap.UserInterfaceButton2Ex[THardware] +} + +func (w *entryWrapper[THardware]) Start(e europi.Hardware) { + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.screen.Start(pi) +} + +func (w *entryWrapper[THardware]) Paint(e europi.Hardware, deltaTime time.Duration) { + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.screen.Paint(pi, deltaTime) +} + +func (w *entryWrapper[THardware]) Button1(e europi.Hardware, deltaTime time.Duration) { + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.button1.Button1(pi, deltaTime) +} + +func (w *entryWrapper[THardware]) Button1Ex(e europi.Hardware, value bool, deltaTime time.Duration) { + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.button1Ex.Button1Ex(pi, value, deltaTime) +} + +func (w *entryWrapper[THardware]) Button1Long(e europi.Hardware, deltaTime time.Duration) { + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.button1Long.Button1Long(pi, deltaTime) +} + +func (w *entryWrapper[THardware]) Button2(e europi.Hardware, deltaTime time.Duration) { + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.button2.Button2(pi, deltaTime) +} + +func (w *entryWrapper[THardware]) Button2Ex(e europi.Hardware, value bool, deltaTime time.Duration) { + pi, ok := e.(THardware) + if !ok { + panic("incorrect hardware type conversion") + } + w.button2Ex.Button2Ex(pi, value, deltaTime) +} diff --git a/experimental/screenbank/screenbankoptions.go b/experimental/screenbank/options.go similarity index 53% rename from experimental/screenbank/screenbankoptions.go rename to experimental/screenbank/options.go index 8b574c7..cc9c00e 100644 --- a/experimental/screenbank/screenbankoptions.go +++ b/experimental/screenbank/options.go @@ -18,7 +18,7 @@ func WithScreen(name string, logo string, screen any) ScreenBankOption { if !ok { return fmt.Errorf("screen %q does not implement a variant of bootstrap.UserInterface", name) } - e := screenBankEntry{ + e := entry{ name: name, logo: logo, screen: details, @@ -32,7 +32,7 @@ func WithScreen(name string, logo string, screen any) ScreenBankOption { } } -func getScreen(screen any) (details screenBankEntryDetails, ok bool) { +func getScreen(screen any) (details entryWrapper[europi.Hardware], ok bool) { if s, _ := screen.(bootstrap.UserInterface[europi.Hardware]); s != nil { details.screen = s details.button1, _ = screen.(bootstrap.UserInterfaceButton1[europi.Hardware]) @@ -58,13 +58,13 @@ func getScreen(screen any) (details screenBankEntryDetails, ok bool) { return } -func getScreenForHardware[THardware europi.Hardware](screen any) (details screenBankEntryDetails, ok bool) { +func getScreenForHardware[THardware europi.Hardware](screen any) (details entryWrapper[europi.Hardware], ok bool) { s, _ := screen.(bootstrap.UserInterface[THardware]) if s == nil { return } - wrapper := &screenHardwareWrapper[THardware]{ + wrapper := &entryWrapper[THardware]{ screen: s, } @@ -89,68 +89,3 @@ func getScreenForHardware[THardware europi.Hardware](screen any) (details screen ok = true return } - -type screenHardwareWrapper[THardware europi.Hardware] struct { - screen bootstrap.UserInterface[THardware] - button1 bootstrap.UserInterfaceButton1[THardware] - button1Ex bootstrap.UserInterfaceButton1Ex[THardware] - button1Long bootstrap.UserInterfaceButton1Long[THardware] - button2 bootstrap.UserInterfaceButton2[THardware] - button2Ex bootstrap.UserInterfaceButton2Ex[THardware] -} - -func (w *screenHardwareWrapper[THardware]) Start(e europi.Hardware) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.screen.Start(pi) -} - -func (w *screenHardwareWrapper[THardware]) Paint(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.screen.Paint(pi, deltaTime) -} - -func (w *screenHardwareWrapper[THardware]) Button1(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button1.Button1(pi, deltaTime) -} - -func (w *screenHardwareWrapper[THardware]) Button1Ex(e europi.Hardware, value bool, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button1Ex.Button1Ex(pi, value, deltaTime) -} - -func (w *screenHardwareWrapper[THardware]) Button1Long(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button1Long.Button1Long(pi, deltaTime) -} - -func (w *screenHardwareWrapper[THardware]) Button2(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button2.Button2(pi, deltaTime) -} - -func (w *screenHardwareWrapper[THardware]) Button2Ex(e europi.Hardware, value bool, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button2Ex.Button2Ex(pi, value, deltaTime) -} diff --git a/experimental/screenbank/screenbank.go b/experimental/screenbank/screenbank.go index 07227bc..354beeb 100644 --- a/experimental/screenbank/screenbank.go +++ b/experimental/screenbank/screenbank.go @@ -11,7 +11,7 @@ import ( type ScreenBank struct { current int - bank []screenBankEntry + bank []entry writer fontwriter.Writer } @@ -42,7 +42,7 @@ func (sb *ScreenBank) CurrentName() string { return sb.bank[sb.current].name } -func (sb *ScreenBank) Current() *screenBankEntryDetails { +func (sb *ScreenBank) Current() *entryWrapper[europi.Hardware] { if len(sb.bank) == 0 { return nil } diff --git a/experimental/screenbank/screenbankentry.go b/experimental/screenbank/screenbankentry.go deleted file mode 100644 index 9812a9f..0000000 --- a/experimental/screenbank/screenbankentry.go +++ /dev/null @@ -1,42 +0,0 @@ -package screenbank - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/bootstrap" -) - -type screenBankEntry struct { - name string - logo string - screen screenBankEntryDetails - enabled bool - locked bool - lastUpdate time.Time -} - -func (e *screenBankEntry) lock() { - if e.locked { - return - } - - e.locked = true -} - -func (e *screenBankEntry) unlock() { - if !e.enabled { - return - } - - e.locked = false -} - -type screenBankEntryDetails struct { - screen bootstrap.UserInterface[europi.Hardware] - button1 bootstrap.UserInterfaceButton1[europi.Hardware] - button1Long bootstrap.UserInterfaceButton1Long[europi.Hardware] - button1Ex bootstrap.UserInterfaceButton1Ex[europi.Hardware] - button2 bootstrap.UserInterfaceButton2[europi.Hardware] - button2Ex bootstrap.UserInterfaceButton2Ex[europi.Hardware] -} From 0d614c74f153c9b61b19f40d8defeda50b45077d Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 5 May 2023 15:59:54 -0700 Subject: [PATCH 54/62] add cancellation context directly on the hardware --- bootstrap/bootstrap.go | 30 ++------ hardware/common/contextpi.go | 136 ++++++++++++++++++++++++++++++++--- hardware/hal/hardware.go | 1 + hardware/rev0/platform.go | 3 + hardware/rev1/platform.go | 3 + 5 files changed, 140 insertions(+), 33 deletions(-) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 7dd7ca0..101d1e6 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -9,13 +9,6 @@ import ( europi "github.com/awonak/EuroPiGo" ) -var ( - // Pi is a global EuroPi instance constructed by calling the Bootstrap() function - Pi europi.Hardware - - piWantDestroyChan chan any -) - // Bootstrap will set up a global runtime environment (see europi.Pi) func Bootstrap(pi europi.Hardware, options ...BootstrapOption) error { e := pi @@ -71,19 +64,16 @@ func Bootstrap(pi europi.Hardware, options ...BootstrapOption) error { } } - Pi = e - piWantDestroyChan = make(chan any, 1) - var ( onceBootstrapDestroy sync.Once nonPicoWSApi NonPicoWSActivation ) panicHandler := config.panicHandler lastDestroyFunc := config.onBeginDestroyFn - ctx, cancel := context.WithCancel(e.Context()) + ctx := e.Context() runBootstrapDestroy := func() { reason := recover() - cancel() + _ = e.Shutdown(reason) if reason != nil && panicHandler != nil { config.onBeginDestroyFn = func(e europi.Hardware, reason any) { if lastDestroyFunc != nil { @@ -113,13 +103,8 @@ func Bootstrap(pi europi.Hardware, options ...BootstrapOption) error { return nil } -func Shutdown(reason any) error { - if piWantDestroyChan == nil { - return errors.New("cannot shutdown: no available bootstrap") - } - - piWantDestroyChan <- reason - return nil +func Shutdown(e europi.Hardware, reason any) error { + return e.Shutdown(reason) } func bootstrapInitializeComponents(ctx context.Context, config *bootstrapConfig, e europi.Hardware) NonPicoWSActivation { @@ -183,7 +168,7 @@ func bootstrapRunLoopWithDelay(config *bootstrapConfig, e europi.Hardware) { lastTick := time.Now() for { select { - case reason := <-piWantDestroyChan: + case reason := <-e.Context().Done(): panic(reason) case now := <-ticker.C: @@ -201,7 +186,7 @@ func bootstrapRunLoopNoDelay(config *bootstrapConfig, e europi.Hardware) { lastTick := time.Now() for { select { - case reason := <-piWantDestroyChan: + case reason := <-e.Context().Done(): panic(reason) default: @@ -232,9 +217,6 @@ func bootstrapDestroy(config *bootstrapConfig, e europi.Hardware, nonPicoWSApi N _ = display.Display() } - close(piWantDestroyChan) - Pi = nil - if config.onFinishDestroyFn != nil { config.onFinishDestroyFn(e) } diff --git a/hardware/common/contextpi.go b/hardware/common/contextpi.go index 2cb4845..b5bf084 100644 --- a/hardware/common/contextpi.go +++ b/hardware/common/contextpi.go @@ -1,23 +1,141 @@ package common -import "time" +import ( + "context" + "fmt" + "sync" + "sync/atomic" +) // ContextPi gives the EuroPi hardware the components necessary // to perform rudimentary context operations -type ContextPi struct{} +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 (ContextPi) Deadline() (deadline time.Time, ok bool) { - return +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 (ContextPi) Done() <-chan struct{} { - return nil +func (c *ContextPi) Err() error { + c.mu.Lock() + err := c.err + c.mu.Unlock() + return err } -func (ContextPi) Err() error { - return nil +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 (ContextPi) Value(key any) any { +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/hal/hardware.go b/hardware/hal/hardware.go index 71fd1ec..09bfb8b 100644 --- a/hardware/hal/hardware.go +++ b/hardware/hal/hardware.go @@ -35,6 +35,7 @@ const ( // 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 diff --git a/hardware/rev0/platform.go b/hardware/rev0/platform.go index 0b0419a..fed2d71 100644 --- a/hardware/rev0/platform.go +++ b/hardware/rev0/platform.go @@ -138,6 +138,9 @@ func GetHardware[T any](hw hal.HardwareId) T { // 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), diff --git a/hardware/rev1/platform.go b/hardware/rev1/platform.go index 2ab6f2a..0b679c7 100644 --- a/hardware/rev1/platform.go +++ b/hardware/rev1/platform.go @@ -133,6 +133,9 @@ func GetHardware[T any](hw hal.HardwareId) T { // 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), From ba799ba8be3ab3f7161fab6a6a56245a28b51e0b Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 5 May 2023 20:20:43 -0700 Subject: [PATCH 55/62] memory cleanup --- bootstrap/pico_panicdisabled.go | 4 +-- experimental/envelope/map32.go | 41 ++++++++++++++++++++++++++++++- experimental/envelope/map64.go | 41 ++++++++++++++++++++++++++++++- hardware/rev0/analoginput.go | 24 +++++++++--------- hardware/rev0/voltageoutput.go | 22 +++++++++-------- hardware/rev1/analoginput.go | 24 +++++++++--------- hardware/rev1/voltageoutput.go | 22 +++++++++-------- hardware/revisiondetection.go | 11 ++++++--- internal/nonpico/nonpico.go | 4 +-- internal/nonpico/rev0/platform.go | 28 ++++++++------------- internal/nonpico/rev1/platform.go | 11 +-------- internal/pico/pico.go | 4 +-- internal/pico/pwm.go | 23 +++++++++-------- 13 files changed, 167 insertions(+), 92 deletions(-) diff --git a/bootstrap/pico_panicdisabled.go b/bootstrap/pico_panicdisabled.go index eefb6df..4a1aa5c 100644 --- a/bootstrap/pico_panicdisabled.go +++ b/bootstrap/pico_panicdisabled.go @@ -9,12 +9,12 @@ import ( ) func init() { - hardware.OnRevisionDetected() <- func(revision hal.Revision) { + hardware.OnRevisionDetected(func(revision hal.Revision) { switch revision { case hal.RevisionUnknown, hal.EuroPiProto: DefaultPanicHandler = handlePanicLogger default: DefaultPanicHandler = handlePanicDisplayCrash } - } + }) } diff --git a/experimental/envelope/map32.go b/experimental/envelope/map32.go index 6ef00a0..7fc605e 100644 --- a/experimental/envelope/map32.go +++ b/experimental/envelope/map32.go @@ -12,7 +12,7 @@ type envMap32[TIn, TOut lerp.Lerpable] struct { outRoot *remapList[TIn, TOut, float32] } -func NewMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { +func NewLerpMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { if len(points) == 0 { panic("must have at least 1 point") } @@ -57,6 +57,45 @@ func NewMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TO } } +func NewPointMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { + if len(points) == 0 { + panic("must have at least 1 point") + } + + p := make(MapEntryList[TIn, TOut], len(points)) + // make a copy just in case we're dealing with another goroutine's data + copy(p, points) + // ensure it's sorted + sort.Sort(p) + + var rem []remapList[TIn, TOut, float32] + for _, cur := range p { + rem = append(rem, remapList[TIn, TOut, float32]{ + Remapper: lerp.NewRemapPoint[TIn, TOut, float32](cur.Input, cur.Output), + }) + } + last := &p[len(p)-1] + + outSort := make(MapEntryList[TOut, int], len(rem)) + for i, e := range rem { + outSort[i].Input = e.OutputMinimum() + outSort[i].Output = i + } + sort.Sort(outSort) + rootIdx := outSort[0].Output + outRoot := &rem[rootIdx] + for pos := 0; pos < len(rem)-1; pos++ { + cur, next := outSort[pos].Output, outSort[pos+1].Output + rem[cur].nextOut = &rem[next] + } + + return &envMap32[TIn, TOut]{ + rem: rem, + outMax: last.Output, + outRoot: outRoot, + } +} + func (m *envMap32[TIn, TOut]) Remap(value TIn) TOut { for _, r := range m.rem { if value < r.InputMinimum() { diff --git a/experimental/envelope/map64.go b/experimental/envelope/map64.go index 8aab14f..121b95f 100644 --- a/experimental/envelope/map64.go +++ b/experimental/envelope/map64.go @@ -12,7 +12,7 @@ type envMap64[TIn, TOut lerp.Lerpable] struct { outRoot *remapList[TIn, TOut, float64] } -func NewMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { +func NewLerpMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { if len(points) == 0 { panic("must have at least 1 point") } @@ -57,6 +57,45 @@ func NewMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TO } } +func NewPointMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { + if len(points) == 0 { + panic("must have at least 1 point") + } + + p := make(MapEntryList[TIn, TOut], len(points)) + // make a copy just in case we're dealing with another goroutine's data + copy(p, points) + // ensure it's sorted + sort.Sort(p) + + var rem []remapList[TIn, TOut, float64] + for _, cur := range p { + rem = append(rem, remapList[TIn, TOut, float64]{ + Remapper: lerp.NewRemapPoint[TIn, TOut, float64](cur.Input, cur.Output), + }) + } + last := &p[len(p)-1] + + outSort := make(MapEntryList[TOut, int], len(rem)) + for i, e := range rem { + outSort[i].Input = e.OutputMinimum() + outSort[i].Output = i + } + sort.Sort(outSort) + rootIdx := outSort[0].Output + outRoot := &rem[rootIdx] + for pos := 0; pos < len(rem)-1; pos++ { + cur, next := outSort[pos].Output, outSort[pos+1].Output + rem[cur].nextOut = &rem[next] + } + + return &envMap64[TIn, TOut]{ + rem: rem, + outMax: last.Output, + outRoot: outRoot, + } +} + func (m *envMap64[TIn, TOut]) Remap(value TIn) TOut { for _, r := range m.rem { if value < r.InputMinimum() { diff --git a/hardware/rev0/analoginput.go b/hardware/rev0/analoginput.go index 476696a..494f1e9 100644 --- a/hardware/rev0/analoginput.go +++ b/hardware/rev0/analoginput.go @@ -18,17 +18,19 @@ const ( ) var ( + AnalogInputCalibrationPoints = []envelope.MapEntry[uint16, float32]{ + { + Input: DefaultCalibratedMinAI, + Output: MinInputVoltage, + }, + { + Input: DefaultCalibratedMaxAI, + Output: MaxInputVoltage, + }, + } + aiInitialConfig = hal.AnalogInputConfig{ - Samples: DefaultSamples, - Calibration: envelope.NewMap32([]envelope.MapEntry[uint16, float32]{ - { - Input: DefaultCalibratedMinAI, - Output: MinInputVoltage, - }, - { - Input: DefaultCalibratedMaxAI, - Output: MaxInputVoltage, - }, - }), + Samples: DefaultSamples, + Calibration: envelope.NewLerpMap32(AnalogInputCalibrationPoints), } ) diff --git a/hardware/rev0/voltageoutput.go b/hardware/rev0/voltageoutput.go index 8577370..f3b2121 100644 --- a/hardware/rev0/voltageoutput.go +++ b/hardware/rev0/voltageoutput.go @@ -23,18 +23,20 @@ const ( ) var ( + VoltageOutputCalibrationPoints = []envelope.MapEntry[float32, uint16]{ + { + Input: MinOutputVoltage, + Output: CalibratedTop, + }, + { + Input: MaxOutputVoltage, + Output: CalibratedOffset, + }, + } + cvInitialConfig = hal.VoltageOutputConfig{ Period: DefaultPWMPeriod, PerformWavefold: true, - Calibration: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ - { - Input: MinOutputVoltage, - Output: CalibratedTop, - }, - { - Input: MaxOutputVoltage, - Output: CalibratedOffset, - }, - }), + Calibration: envelope.NewLerpMap32(VoltageOutputCalibrationPoints), } ) diff --git a/hardware/rev1/analoginput.go b/hardware/rev1/analoginput.go index 390e4ae..787b9ce 100644 --- a/hardware/rev1/analoginput.go +++ b/hardware/rev1/analoginput.go @@ -18,17 +18,19 @@ const ( ) var ( + AnalogInputCalibrationPoints = []envelope.MapEntry[uint16, float32]{ + { + Input: DefaultCalibratedMinAI, + Output: MinInputVoltage, + }, + { + Input: DefaultCalibratedMaxAI, + Output: MaxInputVoltage, + }, + } + aiInitialConfig = hal.AnalogInputConfig{ - Samples: DefaultSamples, - Calibration: envelope.NewMap32([]envelope.MapEntry[uint16, float32]{ - { - Input: DefaultCalibratedMinAI, - Output: MinInputVoltage, - }, - { - Input: DefaultCalibratedMaxAI, - Output: MaxInputVoltage, - }, - }), + Samples: DefaultSamples, + Calibration: envelope.NewLerpMap32(AnalogInputCalibrationPoints), } ) diff --git a/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go index 77caf53..d49481d 100644 --- a/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -23,18 +23,20 @@ const ( ) var ( + VoltageOutputCalibrationPoints = []envelope.MapEntry[float32, uint16]{ + { + Input: MinOutputVoltage, + Output: CalibratedTop, + }, + { + Input: MaxOutputVoltage, + Output: CalibratedOffset, + }, + } + cvInitialConfig = hal.VoltageOutputConfig{ Period: DefaultPWMPeriod, PerformWavefold: true, - Calibration: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ - { - Input: MinOutputVoltage, - Output: CalibratedTop, - }, - { - Input: MaxOutputVoltage, - Output: CalibratedOffset, - }, - }), + Calibration: envelope.NewLerpMap32(VoltageOutputCalibrationPoints), } ) diff --git a/hardware/revisiondetection.go b/hardware/revisiondetection.go index f8c01fb..6d55ffd 100644 --- a/hardware/revisiondetection.go +++ b/hardware/revisiondetection.go @@ -13,10 +13,10 @@ func GetRevision() hal.Revision { var waitForDetect sync.WaitGroup waitForDetect.Add(1) var detectedRevision hal.Revision - OnRevisionDetected() <- func(revision hal.Revision) { + OnRevisionDetected(func(revision hal.Revision) { detectedRevision = revision waitForDetect.Done() - } + }) waitForDetect.Wait() return detectedRevision } @@ -33,9 +33,12 @@ func ensureOnRevisionDetection() { }) } -func OnRevisionDetected() chan<- func(revision hal.Revision) { +func OnRevisionDetected(fn func(revision hal.Revision)) { + if fn == nil { + return + } ensureOnRevisionDetection() - return onRevisionDetected + onRevisionDetected <- fn } // SetDetectedRevision sets the currently detected hardware revision. diff --git a/internal/nonpico/nonpico.go b/internal/nonpico/nonpico.go index 9dfe85c..d33b3e2 100644 --- a/internal/nonpico/nonpico.go +++ b/internal/nonpico/nonpico.go @@ -29,7 +29,7 @@ func DetectRevision() hal.Revision { } func init() { - hardware.OnRevisionDetected() <- func(revision hal.Revision) { + hardware.OnRevisionDetected(func(revision hal.Revision) { switch revision { case hal.Revision0: initRevision0() @@ -40,5 +40,5 @@ func init() { default: } hardware.SetReady() - } + }) } diff --git a/internal/nonpico/rev0/platform.go b/internal/nonpico/rev0/platform.go index 328ff2c..f809c87 100644 --- a/internal/nonpico/rev0/platform.go +++ b/internal/nonpico/rev0/platform.go @@ -7,29 +7,21 @@ import ( ) func DoInit() { - cvCalMap := envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ - { - Input: rev0.MinOutputVoltage, - Output: rev0.CalibratedTop, - }, - { - Input: rev0.MaxOutputVoltage, - Output: rev0.CalibratedOffset, - }, - }) + ajCalMap := envelope.NewLerpMap32(rev0.VoltageOutputCalibrationPoints) + djCalMap := envelope.NewPointMap32(rev0.VoltageOutputCalibrationPoints) rev0.Initialize(rev0.InitializationParameters{ InputButton1: common.NewNonPicoDigitalReader(bus, rev0.HardwareIdButton1Input), InputButton2: common.NewNonPicoDigitalReader(bus, rev0.HardwareIdButton2Input), InputKnob1: common.NewNonPicoAdc(bus, rev0.HardwareIdKnob1Input), InputKnob2: common.NewNonPicoAdc(bus, rev0.HardwareIdKnob2Input), - OutputAnalog1: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog1Output, cvCalMap), - OutputAnalog2: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog2Output, cvCalMap), - OutputAnalog3: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog3Output, cvCalMap), - OutputAnalog4: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog4Output, cvCalMap), - OutputDigital1: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital1Output, cvCalMap), - OutputDigital2: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital2Output, cvCalMap), - OutputDigital3: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital3Output, cvCalMap), - OutputDigital4: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital4Output, cvCalMap), + OutputAnalog1: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog1Output, ajCalMap), + OutputAnalog2: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog2Output, ajCalMap), + OutputAnalog3: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog3Output, ajCalMap), + OutputAnalog4: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog4Output, ajCalMap), + OutputDigital1: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital1Output, djCalMap), + OutputDigital2: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital2Output, djCalMap), + OutputDigital3: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital3Output, djCalMap), + OutputDigital4: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital4Output, djCalMap), DeviceRandomGenerator1: nil, }) } diff --git a/internal/nonpico/rev1/platform.go b/internal/nonpico/rev1/platform.go index ff8a947..c95f634 100644 --- a/internal/nonpico/rev1/platform.go +++ b/internal/nonpico/rev1/platform.go @@ -7,16 +7,7 @@ import ( ) func DoInit() { - cvCalMap := envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ - { - Input: rev1.MinOutputVoltage, - Output: rev1.CalibratedTop, - }, - { - Input: rev1.MaxOutputVoltage, - Output: rev1.CalibratedOffset, - }, - }) + cvCalMap := envelope.NewLerpMap32(rev1.VoltageOutputCalibrationPoints) rev1.Initialize(rev1.InitializationParameters{ InputDigital1: common.NewNonPicoDigitalReader(bus, rev1.HardwareIdDigital1Input), InputAnalog1: common.NewNonPicoAdc(bus, rev1.HardwareIdAnalog1Input), diff --git a/internal/pico/pico.go b/internal/pico/pico.go index 38943fd..6163eb6 100644 --- a/internal/pico/pico.go +++ b/internal/pico/pico.go @@ -57,7 +57,7 @@ func initRevision2() { } func init() { - hardware.OnRevisionDetected() <- func(revision hal.Revision) { + hardware.OnRevisionDetected(func(revision hal.Revision) { switch revision { case hal.Revision0: initRevision0() @@ -68,5 +68,5 @@ func init() { default: } hardware.SetReady() - } + }) } diff --git a/internal/pico/pwm.go b/internal/pico/pwm.go index eaeb96e..1e7596d 100644 --- a/internal/pico/pwm.go +++ b/internal/pico/pwm.go @@ -14,6 +14,7 @@ import ( "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev0" + "github.com/awonak/EuroPiGo/hardware/rev1" ) type picoPwm struct { @@ -46,20 +47,22 @@ const ( ) func newPicoPwm(pwm pwmGroup, pin machine.Pin, mode picoPwmMode) *picoPwm { + var cal envelope.Map[float32, uint16] + switch mode { + case picoPwmModeAnalogRevision0: + cal = envelope.NewLerpMap32(rev0.VoltageOutputCalibrationPoints) + case picoPwmModeDigitalRevision0: + cal = envelope.NewPointMap32(rev0.VoltageOutputCalibrationPoints) + case picoPwmModeAnalogRevision1: + cal = envelope.NewLerpMap32(rev1.VoltageOutputCalibrationPoints) + default: + panic("unhandled mode") + } p := &picoPwm{ pwm: pwm, pin: pin, period: rev0.DefaultPWMPeriod, - cal: envelope.NewMap32([]envelope.MapEntry[float32, uint16]{ - { - Input: rev0.MinOutputVoltage, - Output: rev0.CalibratedTop, - }, - { - Input: rev0.MaxOutputVoltage, - Output: rev0.CalibratedOffset, - }, - }), + cal: cal, } return p } From 7fe0190e06ac5d0493d017214271379fbe81c743 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 6 May 2023 08:26:25 -0700 Subject: [PATCH 56/62] Common event bus for all revisions of hardware --- internal/nonpico/common/adc.go | 6 +-- internal/nonpico/common/bus.go | 45 +++++++++++++++++ internal/nonpico/common/digitalreader.go | 6 +-- internal/nonpico/common/displayoutput.go | 11 ++--- internal/nonpico/common/pwm.go | 7 +-- internal/nonpico/rev0/listeners.go | 48 +++++------------- internal/nonpico/rev0/platform.go | 24 ++++----- internal/nonpico/rev1/listeners.go | 63 ++++++------------------ internal/nonpico/rev1/platform.go | 26 +++++----- 9 files changed, 105 insertions(+), 131 deletions(-) create mode 100644 internal/nonpico/common/bus.go diff --git a/internal/nonpico/common/adc.go b/internal/nonpico/common/adc.go index 1ffd2f7..d409347 100644 --- a/internal/nonpico/common/adc.go +++ b/internal/nonpico/common/adc.go @@ -12,7 +12,6 @@ import ( ) type nonPicoAdc struct { - bus event.Bus id hal.HardwareId value uint16 } @@ -22,10 +21,9 @@ var ( _ common.ADCProvider = (*nonPicoAdc)(nil) ) -func NewNonPicoAdc(bus event.Bus, id hal.HardwareId) *nonPicoAdc { +func NewNonPicoAdc(id hal.HardwareId) *nonPicoAdc { adc := &nonPicoAdc{ - bus: bus, - id: id, + id: id, } event.Subscribe(bus, fmt.Sprintf("hw_value_%d", id), func(msg HwMessageADCValue) { adc.value = msg.Value 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 index ab1f6a2..6892807 100644 --- a/internal/nonpico/common/digitalreader.go +++ b/internal/nonpico/common/digitalreader.go @@ -12,7 +12,6 @@ import ( ) type nonPicoDigitalReader struct { - bus event.Bus id hal.HardwareId value bool } @@ -22,9 +21,8 @@ var ( _ common.DigitalReaderProvider = (*nonPicoDigitalReader)(nil) ) -func NewNonPicoDigitalReader(bus event.Bus, id hal.HardwareId) *nonPicoDigitalReader { +func NewNonPicoDigitalReader(id hal.HardwareId) *nonPicoDigitalReader { dr := &nonPicoDigitalReader{ - bus: bus, id: id, value: true, // start off in high, as that's actually read as low } @@ -40,7 +38,7 @@ func (d *nonPicoDigitalReader) Get() bool { } func (d *nonPicoDigitalReader) SetHandler(changes hal.ChangeFlags, handler func()) { - event.Subscribe(d.bus, fmt.Sprintf("hw_interrupt_%d", d.id), func(msg HwMessageInterrupt) { + 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/displayoutput.go b/internal/nonpico/common/displayoutput.go index 9ccfcbd..a55a418 100644 --- a/internal/nonpico/common/displayoutput.go +++ b/internal/nonpico/common/displayoutput.go @@ -7,7 +7,6 @@ import ( "fmt" "image/color" - "github.com/awonak/EuroPiGo/event" "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" ) @@ -18,7 +17,6 @@ const ( ) type nonPicoDisplayOutput struct { - bus event.Bus id hal.HardwareId width int16 height int16 @@ -29,9 +27,8 @@ var ( _ common.DisplayProvider = (*nonPicoDisplayOutput)(nil) ) -func NewNonPicoDisplayOutput(bus event.Bus, id hal.HardwareId) *nonPicoDisplayOutput { +func NewNonPicoDisplayOutput(id hal.HardwareId) *nonPicoDisplayOutput { dp := &nonPicoDisplayOutput{ - bus: bus, id: id, width: oledWidth, height: oledHeight, @@ -41,7 +38,7 @@ func NewNonPicoDisplayOutput(bus event.Bus, id hal.HardwareId) *nonPicoDisplayOu } func (d *nonPicoDisplayOutput) ClearBuffer() { - d.bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{ + bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{ Op: HwDisplayOpClearBuffer, }) } @@ -50,14 +47,14 @@ func (d *nonPicoDisplayOutput) Size() (x, y int16) { return d.width, d.height } func (d *nonPicoDisplayOutput) SetPixel(x, y int16, c color.RGBA) { - d.bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{ + 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 { - d.bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{ + bus.Post(fmt.Sprintf("hw_display_%d", d.id), HwMessageDisplay{ Op: HwDisplayOpDisplay, }) return nil diff --git a/internal/nonpico/common/pwm.go b/internal/nonpico/common/pwm.go index 1d44eb7..8455d34 100644 --- a/internal/nonpico/common/pwm.go +++ b/internal/nonpico/common/pwm.go @@ -6,14 +6,12 @@ package common import ( "fmt" - "github.com/awonak/EuroPiGo/event" "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" ) type nonPicoPwm struct { - bus event.Bus id hal.HardwareId cal envelope.Map[float32, uint16] v float32 @@ -24,9 +22,8 @@ var ( _ common.PWMProvider = (*nonPicoPwm)(nil) ) -func NewNonPicoPwm(bus event.Bus, id hal.HardwareId, cal envelope.Map[float32, uint16]) *nonPicoPwm { +func NewNonPicoPwm(id hal.HardwareId, cal envelope.Map[float32, uint16]) *nonPicoPwm { p := &nonPicoPwm{ - bus: bus, id: id, cal: cal, } @@ -40,7 +37,7 @@ func (p *nonPicoPwm) Configure(config hal.VoltageOutputConfig) error { func (p *nonPicoPwm) Set(v float32) { pulseWidth := p.cal.Remap(v) p.v = p.cal.Unmap(pulseWidth) - p.bus.Post(fmt.Sprintf("hw_pwm_%d", p.id), HwMessagePwmValue{ + bus.Post(fmt.Sprintf("hw_pwm_%d", p.id), HwMessagePwmValue{ Value: pulseWidth, Voltage: p.v, }) diff --git a/internal/nonpico/rev0/listeners.go b/internal/nonpico/rev0/listeners.go index abaebfc..88a6516 100644 --- a/internal/nonpico/rev0/listeners.go +++ b/internal/nonpico/rev0/listeners.go @@ -4,35 +4,20 @@ package rev0 import ( - "fmt" "sync" - "github.com/awonak/EuroPiGo/event" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev0" "github.com/awonak/EuroPiGo/internal/nonpico/common" "github.com/awonak/EuroPiGo/lerp" ) -var ( - bus = event.NewBus() -) - func setupDefaultState() { - bus.Post(fmt.Sprintf("hw_value_%d", rev0.HardwareIdButton1Input), common.HwMessageDigitalValue{ - Value: false, - }) - bus.Post(fmt.Sprintf("hw_value_%d", rev0.HardwareIdButton2Input), common.HwMessageDigitalValue{ - Value: false, - }) - - bus.Post(fmt.Sprintf("hw_value_%d", rev0.HardwareIdKnob1Input), common.HwMessageADCValue{ - Value: aiLerp.Lerp(0.5), - }) + common.SetDigitalValue(rev0.HardwareIdButton1Input, false) + common.SetDigitalValue(rev0.HardwareIdButton2Input, false) - bus.Post(fmt.Sprintf("hw_value_%d", rev0.HardwareIdKnob2Input), common.HwMessageADCValue{ - Value: aiLerp.Lerp(0.5), - }) + 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)) { @@ -47,12 +32,9 @@ func setupVoltageOutputListeners(cb func(id hal.HardwareId, voltage float32)) { rev0.HardwareIdDigital4Output, } for _, id := range ids { - fn := func(hid hal.HardwareId) func(common.HwMessagePwmValue) { - return func(msg common.HwMessagePwmValue) { - cb(hid, msg.Voltage) - } - }(id) - event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", id), fn) + common.OnPWMValue(id, func(hid hal.HardwareId, value uint16, voltage float32) { + cb(hid, voltage) + }) } } @@ -64,21 +46,15 @@ func setDigitalInput(id hal.HardwareId, value bool) { prevState, _ := states.Load(id) states.Store(id, value) - bus.Post(fmt.Sprintf("hw_value_%d", id), common.HwMessageDigitalValue{ - Value: value, - }) + common.SetDigitalValue(id, value) if prevState != value { if value { // rising - bus.Post(fmt.Sprintf("hw_interrupt_%d", id), common.HwMessageInterrupt{ - Change: hal.ChangeRising, - }) + common.TriggerInterrupt(id, hal.ChangeRising) } else { // falling - bus.Post(fmt.Sprintf("hw_interrupt_%d", id), common.HwMessageInterrupt{ - Change: hal.ChangeFalling, - }) + common.TriggerInterrupt(id, hal.ChangeFalling) } } } @@ -88,7 +64,5 @@ var ( ) func setAnalogInput(id hal.HardwareId, voltage float32) { - bus.Post(fmt.Sprintf("hw_value_%d", id), common.HwMessageADCValue{ - Value: aiLerp.Lerp(voltage), - }) + common.SetADCValue(id, aiLerp.Lerp(voltage)) } diff --git a/internal/nonpico/rev0/platform.go b/internal/nonpico/rev0/platform.go index f809c87..27dcebe 100644 --- a/internal/nonpico/rev0/platform.go +++ b/internal/nonpico/rev0/platform.go @@ -10,18 +10,18 @@ func DoInit() { ajCalMap := envelope.NewLerpMap32(rev0.VoltageOutputCalibrationPoints) djCalMap := envelope.NewPointMap32(rev0.VoltageOutputCalibrationPoints) rev0.Initialize(rev0.InitializationParameters{ - InputButton1: common.NewNonPicoDigitalReader(bus, rev0.HardwareIdButton1Input), - InputButton2: common.NewNonPicoDigitalReader(bus, rev0.HardwareIdButton2Input), - InputKnob1: common.NewNonPicoAdc(bus, rev0.HardwareIdKnob1Input), - InputKnob2: common.NewNonPicoAdc(bus, rev0.HardwareIdKnob2Input), - OutputAnalog1: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog1Output, ajCalMap), - OutputAnalog2: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog2Output, ajCalMap), - OutputAnalog3: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog3Output, ajCalMap), - OutputAnalog4: common.NewNonPicoPwm(bus, rev0.HardwareIdAnalog4Output, ajCalMap), - OutputDigital1: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital1Output, djCalMap), - OutputDigital2: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital2Output, djCalMap), - OutputDigital3: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital3Output, djCalMap), - OutputDigital4: common.NewNonPicoPwm(bus, rev0.HardwareIdDigital4Output, djCalMap), + 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, ajCalMap), + OutputAnalog2: common.NewNonPicoPwm(rev0.HardwareIdAnalog2Output, ajCalMap), + OutputAnalog3: common.NewNonPicoPwm(rev0.HardwareIdAnalog3Output, ajCalMap), + OutputAnalog4: common.NewNonPicoPwm(rev0.HardwareIdAnalog4Output, ajCalMap), + OutputDigital1: common.NewNonPicoPwm(rev0.HardwareIdDigital1Output, djCalMap), + OutputDigital2: common.NewNonPicoPwm(rev0.HardwareIdDigital2Output, djCalMap), + OutputDigital3: common.NewNonPicoPwm(rev0.HardwareIdDigital3Output, djCalMap), + OutputDigital4: common.NewNonPicoPwm(rev0.HardwareIdDigital4Output, djCalMap), DeviceRandomGenerator1: nil, }) } diff --git a/internal/nonpico/rev1/listeners.go b/internal/nonpico/rev1/listeners.go index a9a8993..cd8a5a7 100644 --- a/internal/nonpico/rev1/listeners.go +++ b/internal/nonpico/rev1/listeners.go @@ -4,62 +4,35 @@ package rev1 import ( - "fmt" "sync" - "github.com/awonak/EuroPiGo/event" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev1" "github.com/awonak/EuroPiGo/internal/nonpico/common" "github.com/awonak/EuroPiGo/lerp" ) -var ( - bus = event.NewBus() -) - func setupDefaultState() { - bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdDigital1Input), common.HwMessageDigitalValue{ - Value: false, - }) - bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdAnalog1Input), common.HwMessageADCValue{ - Value: rev1.DefaultCalibratedMaxAI, - }) + common.SetDigitalValue(rev1.HardwareIdDigital1Input, false) + common.SetADCValue(rev1.HardwareIdAnalog1Input, rev1.DefaultCalibratedMaxAI) - bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdButton1Input), common.HwMessageDigitalValue{ - Value: false, - }) - bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdButton2Input), common.HwMessageDigitalValue{ - Value: false, - }) + common.SetDigitalValue(rev1.HardwareIdButton1Input, false) + common.SetDigitalValue(rev1.HardwareIdButton2Input, false) - bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdKnob1Input), common.HwMessageADCValue{ - Value: aiLerp.Lerp(0.5), - }) - - bus.Post(fmt.Sprintf("hw_value_%d", rev1.HardwareIdKnob2Input), common.HwMessageADCValue{ - Value: aiLerp.Lerp(0.5), - }) + 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++ { - fn := func(hid hal.HardwareId) func(common.HwMessagePwmValue) { - return func(msg common.HwMessagePwmValue) { - cb(hid, msg.Voltage) - } - }(id) - event.Subscribe(bus, fmt.Sprintf("hw_pwm_%d", id), fn) + 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)) { - bus := bus - id := hal.HardwareIdDisplay1Output - event.Subscribe(bus, fmt.Sprintf("hw_display_%d", id), func(msg common.HwMessageDisplay) { - cb(id, msg.Op, msg.Operands) - }) - + common.OnDisplayOutput(hal.HardwareIdDisplay1Output, cb) } var ( @@ -70,21 +43,15 @@ func setDigitalInput(id hal.HardwareId, value bool) { prevState, _ := states.Load(id) states.Store(id, value) - bus.Post(fmt.Sprintf("hw_value_%d", id), common.HwMessageDigitalValue{ - Value: value, - }) + common.SetDigitalValue(id, value) if prevState != value { if value { // rising - bus.Post(fmt.Sprintf("hw_interrupt_%d", id), common.HwMessageInterrupt{ - Change: hal.ChangeRising, - }) + common.TriggerInterrupt(id, hal.ChangeRising) } else { // falling - bus.Post(fmt.Sprintf("hw_interrupt_%d", id), common.HwMessageInterrupt{ - Change: hal.ChangeFalling, - }) + common.TriggerInterrupt(id, hal.ChangeFalling) } } } @@ -94,7 +61,5 @@ var ( ) func setAnalogInput(id hal.HardwareId, voltage float32) { - bus.Post(fmt.Sprintf("hw_value_%d", id), common.HwMessageADCValue{ - Value: aiLerp.Lerp(voltage), - }) + common.SetADCValue(id, aiLerp.Lerp(voltage)) } diff --git a/internal/nonpico/rev1/platform.go b/internal/nonpico/rev1/platform.go index c95f634..bd1687b 100644 --- a/internal/nonpico/rev1/platform.go +++ b/internal/nonpico/rev1/platform.go @@ -9,19 +9,19 @@ import ( func DoInit() { cvCalMap := envelope.NewLerpMap32(rev1.VoltageOutputCalibrationPoints) rev1.Initialize(rev1.InitializationParameters{ - InputDigital1: common.NewNonPicoDigitalReader(bus, rev1.HardwareIdDigital1Input), - InputAnalog1: common.NewNonPicoAdc(bus, rev1.HardwareIdAnalog1Input), - OutputDisplay1: common.NewNonPicoDisplayOutput(bus, rev1.HardwareIdDisplay1Output), - InputButton1: common.NewNonPicoDigitalReader(bus, rev1.HardwareIdButton1Input), - InputButton2: common.NewNonPicoDigitalReader(bus, rev1.HardwareIdButton2Input), - InputKnob1: common.NewNonPicoAdc(bus, rev1.HardwareIdKnob1Input), - InputKnob2: common.NewNonPicoAdc(bus, rev1.HardwareIdKnob2Input), - OutputVoltage1: common.NewNonPicoPwm(bus, rev1.HardwareIdCV1Output, cvCalMap), - OutputVoltage2: common.NewNonPicoPwm(bus, rev1.HardwareIdCV2Output, cvCalMap), - OutputVoltage3: common.NewNonPicoPwm(bus, rev1.HardwareIdCV3Output, cvCalMap), - OutputVoltage4: common.NewNonPicoPwm(bus, rev1.HardwareIdCV4Output, cvCalMap), - OutputVoltage5: common.NewNonPicoPwm(bus, rev1.HardwareIdCV5Output, cvCalMap), - OutputVoltage6: common.NewNonPicoPwm(bus, rev1.HardwareIdCV6Output, cvCalMap), + 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, cvCalMap), + OutputVoltage2: common.NewNonPicoPwm(rev1.HardwareIdCV2Output, cvCalMap), + OutputVoltage3: common.NewNonPicoPwm(rev1.HardwareIdCV3Output, cvCalMap), + OutputVoltage4: common.NewNonPicoPwm(rev1.HardwareIdCV4Output, cvCalMap), + OutputVoltage5: common.NewNonPicoPwm(rev1.HardwareIdCV5Output, cvCalMap), + OutputVoltage6: common.NewNonPicoPwm(rev1.HardwareIdCV6Output, cvCalMap), DeviceRandomGenerator1: nil, }) } From fa23047e3d0ee4b75d178e67db9aa0698d2d9b37 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 6 May 2023 08:27:46 -0700 Subject: [PATCH 57/62] better name for monopolar feature --- hardware/hal/voltageoutput.go | 6 +++--- hardware/rev0/voltageoutput.go | 6 +++--- hardware/rev1/voltageoutput.go | 6 +++--- internal/pico/pwm.go | 18 +++++++++--------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/hardware/hal/voltageoutput.go b/hardware/hal/voltageoutput.go index 8cf11c3..62b1c44 100644 --- a/hardware/hal/voltageoutput.go +++ b/hardware/hal/voltageoutput.go @@ -18,7 +18,7 @@ type VoltageOutput interface { } type VoltageOutputConfig struct { - Period time.Duration - PerformWavefold bool - Calibration envelope.Map[float32, uint16] + Period time.Duration + Monopolar bool + Calibration envelope.Map[float32, uint16] } diff --git a/hardware/rev0/voltageoutput.go b/hardware/rev0/voltageoutput.go index f3b2121..8aa0332 100644 --- a/hardware/rev0/voltageoutput.go +++ b/hardware/rev0/voltageoutput.go @@ -35,8 +35,8 @@ var ( } cvInitialConfig = hal.VoltageOutputConfig{ - Period: DefaultPWMPeriod, - PerformWavefold: true, - Calibration: envelope.NewLerpMap32(VoltageOutputCalibrationPoints), + Period: DefaultPWMPeriod, + Monopolar: true, + Calibration: envelope.NewLerpMap32(VoltageOutputCalibrationPoints), } ) diff --git a/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go index d49481d..3da6c56 100644 --- a/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -35,8 +35,8 @@ var ( } cvInitialConfig = hal.VoltageOutputConfig{ - Period: DefaultPWMPeriod, - PerformWavefold: true, - Calibration: envelope.NewLerpMap32(VoltageOutputCalibrationPoints), + Period: DefaultPWMPeriod, + Monopolar: true, + Calibration: envelope.NewLerpMap32(VoltageOutputCalibrationPoints), } ) diff --git a/internal/pico/pwm.go b/internal/pico/pwm.go index 1e7596d..a43ada5 100644 --- a/internal/pico/pwm.go +++ b/internal/pico/pwm.go @@ -18,13 +18,13 @@ import ( ) type picoPwm struct { - pwm pwmGroup - pin machine.Pin - ch uint8 - v uint32 - period time.Duration - wavefold bool - cal envelope.Map[float32, uint16] + pwm pwmGroup + pin machine.Pin + ch uint8 + v uint32 + period time.Duration + monopolar bool + cal envelope.Map[float32, uint16] } // pwmGroup is an interface for interacting with a machine.pwmGroup @@ -93,13 +93,13 @@ func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error { } p.ch = ch - p.wavefold = config.PerformWavefold + p.monopolar = config.Monopolar return nil } func (p *picoPwm) Set(v float32) { - if p.wavefold { + if p.monopolar { if v < 0.0 { v = -v } From c37400795469dfd9daac6f7856765df2659395b5 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 6 May 2023 09:18:35 -0700 Subject: [PATCH 58/62] cleanup --- ...ns_app_conversion.go => app_conversion.go} | 22 ------ bootstrap/options_app.go | 22 ++++++ europi.go | 11 ++- europi_nonpico.go | 10 ++- europi_pico.go | 10 ++- europi_test.go | 24 +++++-- experimental/screenbank/options.go | 32 --------- experimental/screenbank/screen_conversion.go | 38 ++++++++++ internal/nonpico/nonpico.go | 41 +---------- internal/nonpico/platform.go | 38 ++++++++++ internal/nonpico/revisiondetection.go | 12 ++++ .../{rev0.go => revisiondetection_rev0.go} | 0 .../{rev1.go => revisiondetection_rev1.go} | 0 .../{rev2.go => revisiondetection_rev2.go} | 0 internal/pico/pico.go | 69 +----------------- internal/pico/platform.go | 72 +++++++++++++++++++ nonpico.go | 14 ---- pico.go | 14 ---- revisiondetection.go | 7 -- 19 files changed, 232 insertions(+), 204 deletions(-) rename bootstrap/{options_app_conversion.go => app_conversion.go} (78%) create mode 100644 experimental/screenbank/screen_conversion.go create mode 100644 internal/nonpico/platform.go create mode 100644 internal/nonpico/revisiondetection.go rename internal/nonpico/{rev0.go => revisiondetection_rev0.go} (100%) rename internal/nonpico/{rev1.go => revisiondetection_rev1.go} (100%) rename internal/nonpico/{rev2.go => revisiondetection_rev2.go} (100%) create mode 100644 internal/pico/platform.go delete mode 100644 nonpico.go delete mode 100644 pico.go delete mode 100644 revisiondetection.go diff --git a/bootstrap/options_app_conversion.go b/bootstrap/app_conversion.go similarity index 78% rename from bootstrap/options_app_conversion.go rename to bootstrap/app_conversion.go index d7e6e81..132d0a4 100644 --- a/bootstrap/options_app_conversion.go +++ b/bootstrap/app_conversion.go @@ -20,28 +20,6 @@ func appHardwareWrapper[THardware europi.Hardware](app any) any { } } -func getAppFuncs(e europi.Hardware, app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { - if appStart, _ := app.(ApplicationStart[europi.Hardware]); appStart != nil { - start = appStart.Start - } - if appMainLoop, _ := app.(ApplicationMainLoop[europi.Hardware]); appMainLoop != nil { - mainLoop = appMainLoop.MainLoop - } - if appEnd, _ := app.(ApplicationEnd[europi.Hardware]); appEnd != nil { - end = appEnd.End - } - - switch e.(type) { - case *europi.EuroPiPrototype: - start, mainLoop, end = getWrappedAppFuncs[*europi.EuroPiPrototype](app) - case *europi.EuroPi: - start, mainLoop, end = getWrappedAppFuncs[*europi.EuroPi](app) - // TODO: add rev2 - } - - return -} - func getWrappedAppFuncs[THardware europi.Hardware](app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { appWrapper := appHardwareWrapper[THardware](app) if getStart, _ := appWrapper.(applicationStartProvider); getStart != nil { diff --git a/bootstrap/options_app.go b/bootstrap/options_app.go index 3ac2379..95fd7a1 100644 --- a/bootstrap/options_app.go +++ b/bootstrap/options_app.go @@ -76,3 +76,25 @@ func AppMainLoopInterval(interval time.Duration) BootstrapAppOption { return nil } } + +func getAppFuncs(e europi.Hardware, app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { + if appStart, _ := app.(ApplicationStart[europi.Hardware]); appStart != nil { + start = appStart.Start + } + if appMainLoop, _ := app.(ApplicationMainLoop[europi.Hardware]); appMainLoop != nil { + mainLoop = appMainLoop.MainLoop + } + if appEnd, _ := app.(ApplicationEnd[europi.Hardware]); appEnd != nil { + end = appEnd.End + } + + switch e.(type) { + case *europi.EuroPiPrototype: + start, mainLoop, end = getWrappedAppFuncs[*europi.EuroPiPrototype](app) + case *europi.EuroPi: + start, mainLoop, end = getWrappedAppFuncs[*europi.EuroPi](app) + // TODO: add rev2 + } + + return +} diff --git a/europi.go b/europi.go index f98b8b2..cfadb84 100644 --- a/europi.go +++ b/europi.go @@ -5,6 +5,8 @@ import ( "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" ) type ( @@ -21,7 +23,7 @@ type ( // New will return a new EuroPi struct based on the detected hardware revision func New() Hardware { // ensure our hardware has been identified - EnsureHardware() + ensureHardware() // blocks until revision has been identified revision := hardware.GetRevision() @@ -35,6 +37,9 @@ func NewFrom(revision hal.Revision) Hardware { return nil } + // ensure our hardware has been identified + ensureHardware() + // this will block until the hardware components are initialized hardware.WaitForReady() @@ -104,3 +109,7 @@ func Knob(e Hardware, idx int) hal.KnobInput { } 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 index 4e3933f..ad3ca2e 100644 --- a/europi_nonpico.go +++ b/europi_nonpico.go @@ -4,13 +4,19 @@ package europi import ( + "sync" + "github.com/awonak/EuroPiGo/hardware" "github.com/awonak/EuroPiGo/internal/nonpico" ) +var nonPicoEnsureHardwareOnce sync.Once + func nonPicoEnsureHardware() { - rev := nonpico.DetectRevision() - hardware.SetDetectedRevision(rev) + nonPicoEnsureHardwareOnce.Do(func() { + rev := nonpico.DetectRevision() + hardware.SetDetectedRevision(rev) + }) } func init() { diff --git a/europi_pico.go b/europi_pico.go index 657ae1f..5076242 100644 --- a/europi_pico.go +++ b/europi_pico.go @@ -4,13 +4,19 @@ package europi import ( + "sync" + "github.com/awonak/EuroPiGo/hardware" "github.com/awonak/EuroPiGo/internal/pico" ) +var picoEnsureHardwareOnce sync.Once + func picoEnsureHardware() { - rev := pico.DetectRevision() - hardware.SetDetectedRevision(rev) + picoEnsureHardwareOnce.Do(func() { + rev := pico.DetectRevision() + hardware.SetDetectedRevision(rev) + }) } func init() { diff --git a/europi_test.go b/europi_test.go index 7e68c14..09685e7 100644 --- a/europi_test.go +++ b/europi_test.go @@ -21,15 +21,31 @@ func TestNew(t *testing.T) { t.Run("Revision0", func(t *testing.T) { hardware.SetDetectedRevision(hal.Revision0) - if actual, _ := europi.New().(*rev0.EuroPiPrototype); actual == nil { - t.Fatalf("EuroPi New: expected[EuroPiPrototype] actual[%T]", actual) + 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) - if actual, _ := europi.New().(*rev1.EuroPi); actual == nil { - t.Fatalf("EuroPi New: expected[EuroPi] actual[%T]", actual) + 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) } }) } diff --git a/experimental/screenbank/options.go b/experimental/screenbank/options.go index cc9c00e..fb5e338 100644 --- a/experimental/screenbank/options.go +++ b/experimental/screenbank/options.go @@ -57,35 +57,3 @@ func getScreen(screen any) (details entryWrapper[europi.Hardware], ok bool) { return } - -func getScreenForHardware[THardware europi.Hardware](screen any) (details entryWrapper[europi.Hardware], ok bool) { - s, _ := screen.(bootstrap.UserInterface[THardware]) - if s == nil { - return - } - - wrapper := &entryWrapper[THardware]{ - screen: s, - } - - details.screen = wrapper - - if wrapper.button1, _ = screen.(bootstrap.UserInterfaceButton1[THardware]); wrapper.button1 != nil { - details.button1 = wrapper - } - if wrapper.button1Long, _ = screen.(bootstrap.UserInterfaceButton1Long[THardware]); wrapper.button1Long != nil { - details.button1Long = wrapper - } - if wrapper.button1Ex, _ = screen.(bootstrap.UserInterfaceButton1Ex[THardware]); wrapper.button1Ex != nil { - details.button1Ex = wrapper - } - if wrapper.button2, _ = screen.(bootstrap.UserInterfaceButton2[THardware]); wrapper.button2 != nil { - details.button2 = wrapper - } - if wrapper.button2Ex, _ = screen.(bootstrap.UserInterfaceButton2Ex[THardware]); wrapper.button2Ex != nil { - details.button2Ex = wrapper - } - - ok = true - return -} diff --git a/experimental/screenbank/screen_conversion.go b/experimental/screenbank/screen_conversion.go new file mode 100644 index 0000000..1a90805 --- /dev/null +++ b/experimental/screenbank/screen_conversion.go @@ -0,0 +1,38 @@ +package screenbank + +import ( + europi "github.com/awonak/EuroPiGo" + "github.com/awonak/EuroPiGo/bootstrap" +) + +func getScreenForHardware[THardware europi.Hardware](screen any) (details entryWrapper[europi.Hardware], ok bool) { + s, _ := screen.(bootstrap.UserInterface[THardware]) + if s == nil { + return + } + + wrapper := &entryWrapper[THardware]{ + screen: s, + } + + details.screen = wrapper + + if wrapper.button1, _ = screen.(bootstrap.UserInterfaceButton1[THardware]); wrapper.button1 != nil { + details.button1 = wrapper + } + if wrapper.button1Long, _ = screen.(bootstrap.UserInterfaceButton1Long[THardware]); wrapper.button1Long != nil { + details.button1Long = wrapper + } + if wrapper.button1Ex, _ = screen.(bootstrap.UserInterfaceButton1Ex[THardware]); wrapper.button1Ex != nil { + details.button1Ex = wrapper + } + if wrapper.button2, _ = screen.(bootstrap.UserInterfaceButton2[THardware]); wrapper.button2 != nil { + details.button2 = wrapper + } + if wrapper.button2Ex, _ = screen.(bootstrap.UserInterfaceButton2Ex[THardware]); wrapper.button2Ex != nil { + details.button2Ex = wrapper + } + + ok = true + return +} diff --git a/internal/nonpico/nonpico.go b/internal/nonpico/nonpico.go index d33b3e2..87e6ad2 100644 --- a/internal/nonpico/nonpico.go +++ b/internal/nonpico/nonpico.go @@ -1,44 +1,7 @@ -//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() -} - -var detectedRevision hal.Revision - -func DetectRevision() hal.Revision { - return detectedRevision -} +// 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() { - 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/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/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/rev0.go b/internal/nonpico/revisiondetection_rev0.go similarity index 100% rename from internal/nonpico/rev0.go rename to internal/nonpico/revisiondetection_rev0.go diff --git a/internal/nonpico/rev1.go b/internal/nonpico/revisiondetection_rev1.go similarity index 100% rename from internal/nonpico/rev1.go rename to internal/nonpico/revisiondetection_rev1.go diff --git a/internal/nonpico/rev2.go b/internal/nonpico/revisiondetection_rev2.go similarity index 100% rename from internal/nonpico/rev2.go rename to internal/nonpico/revisiondetection_rev2.go diff --git a/internal/pico/pico.go b/internal/pico/pico.go index 6163eb6..4d10075 100644 --- a/internal/pico/pico.go +++ b/internal/pico/pico.go @@ -1,72 +1,7 @@ -//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), - InputKnob2: newPicoAdc(machine.ADC1), - OutputAnalog1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeDigitalRevision0), - OutputAnalog2: newPicoPwm(machine.PWM3, machine.GPIO22, picoPwmModeDigitalRevision0), - OutputAnalog3: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeDigitalRevision0), - OutputAnalog4: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeDigitalRevision0), - OutputDigital1: newPicoPwm(machine.PWM7, machine.GPIO14, picoPwmModeAnalogRevision0), - OutputDigital2: newPicoPwm(machine.PWM5, machine.GPIO11, picoPwmModeAnalogRevision0), - OutputDigital3: newPicoPwm(machine.PWM5, machine.GPIO10, picoPwmModeAnalogRevision0), - OutputDigital4: newPicoPwm(machine.PWM3, machine.GPIO7, picoPwmModeAnalogRevision0), - DeviceRandomGenerator1: &picoRnd{}, - }) -} - -// EuroPi (original) -func initRevision1() { - rev1.Initialize(rev1.InitializationParameters{ - InputDigital1: newPicoDigitalReader(machine.GPIO22), - InputAnalog1: newPicoAdc(machine.ADC0), - OutputDisplay1: newPicoDisplayOutput(machine.I2C0, machine.GPIO0, machine.GPIO1), - InputButton1: newPicoDigitalReader(machine.GPIO4), - InputButton2: newPicoDigitalReader(machine.GPIO5), - InputKnob1: newPicoAdc(machine.ADC1), - InputKnob2: newPicoAdc(machine.ADC2), - OutputVoltage1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeAnalogRevision1), - OutputVoltage2: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeAnalogRevision1), - OutputVoltage3: newPicoPwm(machine.PWM0, machine.GPIO16, picoPwmModeAnalogRevision1), - OutputVoltage4: newPicoPwm(machine.PWM0, machine.GPIO17, picoPwmModeAnalogRevision1), - OutputVoltage5: newPicoPwm(machine.PWM1, machine.GPIO18, picoPwmModeAnalogRevision1), - OutputVoltage6: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeAnalogRevision1), - DeviceRandomGenerator1: &picoRnd{}, - }) -} - -// EuroPi-X -func initRevision2() { - // TODO: initialize hardware -} +// 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() { - 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/platform.go b/internal/pico/platform.go new file mode 100644 index 0000000..6163eb6 --- /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), + InputKnob2: newPicoAdc(machine.ADC1), + OutputAnalog1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeDigitalRevision0), + OutputAnalog2: newPicoPwm(machine.PWM3, machine.GPIO22, picoPwmModeDigitalRevision0), + OutputAnalog3: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeDigitalRevision0), + OutputAnalog4: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeDigitalRevision0), + OutputDigital1: newPicoPwm(machine.PWM7, machine.GPIO14, picoPwmModeAnalogRevision0), + OutputDigital2: newPicoPwm(machine.PWM5, machine.GPIO11, picoPwmModeAnalogRevision0), + OutputDigital3: newPicoPwm(machine.PWM5, machine.GPIO10, picoPwmModeAnalogRevision0), + OutputDigital4: newPicoPwm(machine.PWM3, machine.GPIO7, picoPwmModeAnalogRevision0), + DeviceRandomGenerator1: &picoRnd{}, + }) +} + +// EuroPi (original) +func initRevision1() { + rev1.Initialize(rev1.InitializationParameters{ + InputDigital1: newPicoDigitalReader(machine.GPIO22), + InputAnalog1: newPicoAdc(machine.ADC0), + OutputDisplay1: newPicoDisplayOutput(machine.I2C0, machine.GPIO0, machine.GPIO1), + InputButton1: newPicoDigitalReader(machine.GPIO4), + InputButton2: newPicoDigitalReader(machine.GPIO5), + InputKnob1: newPicoAdc(machine.ADC1), + InputKnob2: newPicoAdc(machine.ADC2), + OutputVoltage1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeAnalogRevision1), + OutputVoltage2: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeAnalogRevision1), + OutputVoltage3: newPicoPwm(machine.PWM0, machine.GPIO16, picoPwmModeAnalogRevision1), + OutputVoltage4: newPicoPwm(machine.PWM0, machine.GPIO17, picoPwmModeAnalogRevision1), + OutputVoltage5: newPicoPwm(machine.PWM1, machine.GPIO18, picoPwmModeAnalogRevision1), + OutputVoltage6: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeAnalogRevision1), + 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/nonpico.go b/nonpico.go deleted file mode 100644 index 056e0b6..0000000 --- a/nonpico.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !pico -// +build !pico - -package europi - -import ( - _ "github.com/awonak/EuroPiGo/internal/nonpico" -) - -// This file exists to import the non-pico code into the active build -// do not remove this file or remove the init() function below - -func init() { -} diff --git a/pico.go b/pico.go deleted file mode 100644 index a2e23bd..0000000 --- a/pico.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build pico -// +build pico - -package europi - -import ( - _ "github.com/awonak/EuroPiGo/internal/pico" -) - -// This file exists to import the pico code into the active build -// do not remove this file or remove the init() function below - -func init() { -} diff --git a/revisiondetection.go b/revisiondetection.go deleted file mode 100644 index 035816e..0000000 --- a/revisiondetection.go +++ /dev/null @@ -1,7 +0,0 @@ -package europi - -var ensureHardware func() - -func EnsureHardware() { - ensureHardware() -} From b87eac003a6bdd2dec7b67336b70b9c5743abb33 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 6 May 2023 12:41:11 -0700 Subject: [PATCH 59/62] simplify pico pwm interface --- internal/pico/platform.go | 38 +++++++++++++++++++------------------- internal/pico/pwm.go | 32 +++++++++----------------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/internal/pico/platform.go b/internal/pico/platform.go index 6163eb6..f7cb297 100644 --- a/internal/pico/platform.go +++ b/internal/pico/platform.go @@ -17,16 +17,16 @@ func initRevision0() { rev0.Initialize(rev0.InitializationParameters{ InputButton1: newPicoDigitalReader(machine.GPIO15), InputButton2: newPicoDigitalReader(machine.GPIO18), - InputKnob1: newPicoAdc(machine.ADC2), - InputKnob2: newPicoAdc(machine.ADC1), - OutputAnalog1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeDigitalRevision0), - OutputAnalog2: newPicoPwm(machine.PWM3, machine.GPIO22, picoPwmModeDigitalRevision0), - OutputAnalog3: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeDigitalRevision0), - OutputAnalog4: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeDigitalRevision0), - OutputDigital1: newPicoPwm(machine.PWM7, machine.GPIO14, picoPwmModeAnalogRevision0), - OutputDigital2: newPicoPwm(machine.PWM5, machine.GPIO11, picoPwmModeAnalogRevision0), - OutputDigital3: newPicoPwm(machine.PWM5, machine.GPIO10, picoPwmModeAnalogRevision0), - OutputDigital4: newPicoPwm(machine.PWM3, machine.GPIO7, picoPwmModeAnalogRevision0), + 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{}, }) } @@ -35,18 +35,18 @@ func initRevision0() { func initRevision1() { rev1.Initialize(rev1.InitializationParameters{ InputDigital1: newPicoDigitalReader(machine.GPIO22), - InputAnalog1: newPicoAdc(machine.ADC0), + 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), - InputKnob2: newPicoAdc(machine.ADC2), - OutputVoltage1: newPicoPwm(machine.PWM2, machine.GPIO21, picoPwmModeAnalogRevision1), - OutputVoltage2: newPicoPwm(machine.PWM2, machine.GPIO20, picoPwmModeAnalogRevision1), - OutputVoltage3: newPicoPwm(machine.PWM0, machine.GPIO16, picoPwmModeAnalogRevision1), - OutputVoltage4: newPicoPwm(machine.PWM0, machine.GPIO17, picoPwmModeAnalogRevision1), - OutputVoltage5: newPicoPwm(machine.PWM1, machine.GPIO18, picoPwmModeAnalogRevision1), - OutputVoltage6: newPicoPwm(machine.PWM1, machine.GPIO19, picoPwmModeAnalogRevision1), + 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{}, }) } diff --git a/internal/pico/pwm.go b/internal/pico/pwm.go index a43ada5..e35ee96 100644 --- a/internal/pico/pwm.go +++ b/internal/pico/pwm.go @@ -8,13 +8,12 @@ import ( "machine" "math" "runtime/interrupt" - "runtime/volatile" + "sync/atomic" "time" "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev0" - "github.com/awonak/EuroPiGo/hardware/rev1" ) type picoPwm struct { @@ -40,29 +39,12 @@ type pwmGroup interface { type picoPwmMode int -const ( - picoPwmModeAnalogRevision0 = picoPwmMode(iota) - picoPwmModeDigitalRevision0 - picoPwmModeAnalogRevision1 -) - -func newPicoPwm(pwm pwmGroup, pin machine.Pin, mode picoPwmMode) *picoPwm { - var cal envelope.Map[float32, uint16] - switch mode { - case picoPwmModeAnalogRevision0: - cal = envelope.NewLerpMap32(rev0.VoltageOutputCalibrationPoints) - case picoPwmModeDigitalRevision0: - cal = envelope.NewPointMap32(rev0.VoltageOutputCalibrationPoints) - case picoPwmModeAnalogRevision1: - cal = envelope.NewLerpMap32(rev1.VoltageOutputCalibrationPoints) - default: - panic("unhandled mode") - } +func newPicoPwm(pwm pwmGroup, pin machine.Pin) *picoPwm { p := &picoPwm{ pwm: pwm, pin: pin, period: rev0.DefaultPWMPeriod, - cal: cal, + // NOTE: cal must be set non-nil by Configure() at least 1 time } return p } @@ -86,6 +68,10 @@ func (p *picoPwm) Configure(config hal.VoltageOutputConfig) error { 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 { @@ -108,11 +94,11 @@ func (p *picoPwm) Set(v float32) { state := interrupt.Disable() p.pwm.Set(p.ch, uint32(volts)) interrupt.Restore(state) - volatile.StoreUint32(&p.v, math.Float32bits(v)) + atomic.StoreUint32(&p.v, math.Float32bits(v)) } func (p *picoPwm) Get() float32 { - return math.Float32frombits(volatile.LoadUint32(&p.v)) + return math.Float32frombits(atomic.LoadUint32(&p.v)) } func (p *picoPwm) MinVoltage() float32 { From 787c7fad031e63efd9edd9c9d6648d9923c190da Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 6 May 2023 13:24:25 -0700 Subject: [PATCH 60/62] bifurcating components specific to bootstrap out - see github.com/heucuva/europi-bootstrap - this slims down the firmware-specific code significantly --- .gitignore | 6 + bootstrap/app_conversion.go | 100 -------- bootstrap/bootstrap.go | 223 ------------------ bootstrap/features.go | 71 ------ bootstrap/lifecycle.go | 37 --- bootstrap/nonpico.go | 30 --- bootstrap/nonpico_panic.go | 8 - bootstrap/nonpico_websimenabled.go | 8 - bootstrap/options.go | 28 --- bootstrap/options_app.go | 100 -------- bootstrap/options_features.go | 40 ---- bootstrap/options_lifecycle.go | 161 ------------- bootstrap/options_ui.go | 53 ----- bootstrap/panic.go | 62 ----- bootstrap/pico_panicdisabled.go | 20 -- bootstrap/pico_panicenabled.go | 8 - bootstrap/ui.go | 76 ------ bootstrap/uimodule.go | 193 --------------- debug.go | 17 ++ debug_nonpico.go | 19 ++ experimental/knobmenu/item.go | 10 - experimental/knobmenu/knobmenu.go | 107 --------- experimental/knobmenu/options.go | 54 ----- experimental/screenbank/entry.go | 32 --- experimental/screenbank/entrywrapper.go | 73 ------ experimental/screenbank/options.go | 59 ----- experimental/screenbank/screen_conversion.go | 38 --- experimental/screenbank/screenbank.go | 156 ------------ internal/projects/clockgenerator/LICENSE.md | 11 - internal/projects/clockgenerator/README.md | 46 ---- .../projects/clockgenerator/clockgenerator.go | 93 -------- .../projects/clockgenerator/module/config.go | 14 -- .../projects/clockgenerator/module/module.go | 119 ---------- .../clockgenerator/module/setting_bpm.go | 29 --- .../module/setting_gateduration.go | 29 --- .../projects/clockgenerator/screen/main.go | 50 ---- .../clockgenerator/screen/settings.go | 63 ----- internal/projects/clockwerk/clockwerk.go | 3 +- internal/projects/diagnostics/diagnostics.go | 3 +- internal/projects/randomskips/LICENSE.md | 11 - internal/projects/randomskips/README.md | 57 ----- .../projects/randomskips/module/config.go | 6 - .../projects/randomskips/module/module.go | 65 ----- .../randomskips/module/setting_chance.go | 20 -- internal/projects/randomskips/randomskips.go | 119 ---------- internal/projects/randomskips/screen/main.go | 52 ---- .../projects/randomskips/screen/settings.go | 50 ---- 47 files changed, 44 insertions(+), 2585 deletions(-) delete mode 100644 bootstrap/app_conversion.go delete mode 100644 bootstrap/bootstrap.go delete mode 100644 bootstrap/features.go delete mode 100644 bootstrap/lifecycle.go delete mode 100644 bootstrap/nonpico.go delete mode 100644 bootstrap/nonpico_panic.go delete mode 100644 bootstrap/nonpico_websimenabled.go delete mode 100644 bootstrap/options.go delete mode 100644 bootstrap/options_app.go delete mode 100644 bootstrap/options_features.go delete mode 100644 bootstrap/options_lifecycle.go delete mode 100644 bootstrap/options_ui.go delete mode 100644 bootstrap/panic.go delete mode 100644 bootstrap/pico_panicdisabled.go delete mode 100644 bootstrap/pico_panicenabled.go delete mode 100644 bootstrap/ui.go delete mode 100644 bootstrap/uimodule.go create mode 100644 debug_nonpico.go delete mode 100644 experimental/knobmenu/item.go delete mode 100644 experimental/knobmenu/knobmenu.go delete mode 100644 experimental/knobmenu/options.go delete mode 100644 experimental/screenbank/entry.go delete mode 100644 experimental/screenbank/entrywrapper.go delete mode 100644 experimental/screenbank/options.go delete mode 100644 experimental/screenbank/screen_conversion.go delete mode 100644 experimental/screenbank/screenbank.go delete mode 100644 internal/projects/clockgenerator/LICENSE.md delete mode 100644 internal/projects/clockgenerator/README.md delete mode 100644 internal/projects/clockgenerator/clockgenerator.go delete mode 100644 internal/projects/clockgenerator/module/config.go delete mode 100644 internal/projects/clockgenerator/module/module.go delete mode 100644 internal/projects/clockgenerator/module/setting_bpm.go delete mode 100644 internal/projects/clockgenerator/module/setting_gateduration.go delete mode 100644 internal/projects/clockgenerator/screen/main.go delete mode 100644 internal/projects/clockgenerator/screen/settings.go delete mode 100644 internal/projects/randomskips/LICENSE.md delete mode 100644 internal/projects/randomskips/README.md delete mode 100644 internal/projects/randomskips/module/config.go delete mode 100644 internal/projects/randomskips/module/module.go delete mode 100644 internal/projects/randomskips/module/setting_chance.go delete mode 100644 internal/projects/randomskips/randomskips.go delete mode 100644 internal/projects/randomskips/screen/main.go delete mode 100644 internal/projects/randomskips/screen/settings.go diff --git a/.gitignore b/.gitignore index a3a2076..658eed1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# 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~ @@ -15,6 +18,9 @@ __debug_bin # Dependency directories (remove the comment below to include it) # vendor/ +# Go workspace file +go.work + # TinyGo *.elf *.uf2 diff --git a/bootstrap/app_conversion.go b/bootstrap/app_conversion.go deleted file mode 100644 index 132d0a4..0000000 --- a/bootstrap/app_conversion.go +++ /dev/null @@ -1,100 +0,0 @@ -package bootstrap - -import ( - "errors" - "time" - - europi "github.com/awonak/EuroPiGo" -) - -// appHardwareWrapper sets up a wrapper around an app that expects a particular hardware interface -// this is for automated parameter interpretation -func appHardwareWrapper[THardware europi.Hardware](app any) any { - start, _ := app.(ApplicationStart[THardware]) - mainLoop, _ := app.(ApplicationMainLoop[THardware]) - end, _ := app.(ApplicationEnd[THardware]) - return &appWrapper[THardware]{ - start: start, - mainLoop: mainLoop, - end: end, - } -} - -func getWrappedAppFuncs[THardware europi.Hardware](app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { - appWrapper := appHardwareWrapper[THardware](app) - if getStart, _ := appWrapper.(applicationStartProvider); getStart != nil { - start = getStart.ApplicationStart() - } - - if getMainLoop, _ := appWrapper.(applicationMainLoopProvider); getMainLoop != nil { - mainLoop = getMainLoop.ApplicationMainLoop() - } - - if getEnd, _ := appWrapper.(applicationEndProvider); getEnd != nil { - end = getEnd.ApplicationEnd() - } - return -} - -type applicationStartProvider interface { - ApplicationStart() AppStartFunc -} - -type applicationMainLoopProvider interface { - ApplicationMainLoop() AppMainLoopFunc -} - -type applicationEndProvider interface { - ApplicationEnd() AppEndFunc -} - -type appWrapper[THardware europi.Hardware] struct { - start ApplicationStart[THardware] - mainLoop ApplicationMainLoop[THardware] - end ApplicationEnd[THardware] -} - -func (a *appWrapper[THardware]) ApplicationStart() AppStartFunc { - if a.start == nil { - return nil - } - return a.doStart -} - -func (a *appWrapper[THardware]) doStart(e europi.Hardware) { - pi, ok := e.(THardware) - if !ok { - panic(errors.New("incorrect hardware type conversion")) - } - a.start.Start(pi) -} - -func (a *appWrapper[THardware]) ApplicationMainLoop() AppMainLoopFunc { - if a.mainLoop == nil { - return nil - } - return a.doMainLoop -} - -func (a *appWrapper[THardware]) doMainLoop(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic(errors.New("incorrect hardware type conversion")) - } - a.mainLoop.MainLoop(pi, deltaTime) -} - -func (a *appWrapper[THardware]) ApplicationEnd() AppEndFunc { - if a.end == nil { - return nil - } - return a.doEnd -} - -func (a *appWrapper[THardware]) doEnd(e europi.Hardware) { - pi, ok := e.(THardware) - if !ok { - panic(errors.New("incorrect hardware type conversion")) - } - a.end.End(pi) -} diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go deleted file mode 100644 index 101d1e6..0000000 --- a/bootstrap/bootstrap.go +++ /dev/null @@ -1,223 +0,0 @@ -package bootstrap - -import ( - "context" - "errors" - "sync" - "time" - - europi "github.com/awonak/EuroPiGo" -) - -// Bootstrap will set up a global runtime environment (see europi.Pi) -func Bootstrap(pi europi.Hardware, options ...BootstrapOption) error { - e := pi - if e == nil { - return errors.New("europi must be provided") - } - - config := bootstrapConfig{ - panicHandler: DefaultPanicHandler, - enableDisplayLogger: DefaultEnableDisplayLogger, - initRandom: DefaultInitRandom, - enableNonPicoWebSocket: defaultWebSimEnabled, - europi: e, - - appConfig: bootstrapAppConfig{ - mainLoopInterval: DefaultAppMainLoopInterval, - onAppStartFn: nil, - onAppMainLoopFn: DefaultMainLoop, - onAppEndFn: nil, - }, - - uiConfig: bootstrapUIConfig{ - ui: nil, - uiRefreshRate: DefaultUIRefreshRate, - }, - - onPostBootstrapConstructionFn: DefaultPostBootstrapInitialization, - onPreInitializeComponentsFn: nil, - onPostInitializeComponentsFn: nil, - onBootstrapCompletedFn: DefaultBootstrapCompleted, - onBeginDestroyFn: nil, - onFinishDestroyFn: nil, - } - - // process bootstrap options - for _, opt := range options { - if err := opt(&config); err != nil { - return err - } - } - - // process app options - for _, opt := range config.appConfig.options { - if err := opt(&config.appConfig); err != nil { - return err - } - } - - // process ui options - for _, opt := range config.uiConfig.options { - if err := opt(&config.uiConfig); err != nil { - return err - } - } - - var ( - onceBootstrapDestroy sync.Once - nonPicoWSApi NonPicoWSActivation - ) - panicHandler := config.panicHandler - lastDestroyFunc := config.onBeginDestroyFn - ctx := e.Context() - runBootstrapDestroy := func() { - reason := recover() - _ = e.Shutdown(reason) - if reason != nil && panicHandler != nil { - config.onBeginDestroyFn = func(e europi.Hardware, reason any) { - if lastDestroyFunc != nil { - lastDestroyFunc(e, reason) - } - panicHandler(e, reason) - } - } - onceBootstrapDestroy.Do(func() { - bootstrapDestroy(&config, e, nonPicoWSApi, reason) - }) - } - defer runBootstrapDestroy() - - if config.onPostBootstrapConstructionFn != nil { - config.onPostBootstrapConstructionFn(e) - } - - nonPicoWSApi = bootstrapInitializeComponents(ctx, &config, e) - - if config.onBootstrapCompletedFn != nil { - config.onBootstrapCompletedFn(e) - } - - bootstrapRunLoop(&config, e) - - return nil -} - -func Shutdown(e europi.Hardware, reason any) error { - return e.Shutdown(reason) -} - -func bootstrapInitializeComponents(ctx context.Context, config *bootstrapConfig, e europi.Hardware) NonPicoWSActivation { - if config.onPreInitializeComponentsFn != nil { - config.onPreInitializeComponentsFn(e) - } - - if config.enableDisplayLogger { - enableDisplayLogger(e) - } - - var nonPicoWSApi NonPicoWSActivation - if config.enableNonPicoWebSocket { - nonPicoWSApi = ActivateNonPicoWS(ctx, e) - } - - if config.initRandom { - initRandom(e) - } - - // ui initializaiton is always last - if config.uiConfig.ui != nil { - enableUI(ctx, e, config.uiConfig) - } - - if config.onPostInitializeComponentsFn != nil { - config.onPostInitializeComponentsFn(e) - } - - return nonPicoWSApi -} - -func bootstrapRunLoop(config *bootstrapConfig, e europi.Hardware) { - if config.appConfig.onAppStartFn != nil { - config.appConfig.onAppStartFn(e) - } - - startUI(e) - - ForceRepaintUI(e) - - if config.appConfig.mainLoopInterval > 0 { - bootstrapRunLoopWithDelay(config, e) - } else { - bootstrapRunLoopNoDelay(config, e) - } - - if config.appConfig.onAppEndFn != nil { - config.appConfig.onAppEndFn(e) - } -} - -func bootstrapRunLoopWithDelay(config *bootstrapConfig, e europi.Hardware) { - if config.appConfig.onAppMainLoopFn == nil { - panic(errors.New("no main loop specified")) - } - - ticker := time.NewTicker(config.appConfig.mainLoopInterval) - defer ticker.Stop() - - lastTick := time.Now() - for { - select { - case reason := <-e.Context().Done(): - panic(reason) - - case now := <-ticker.C: - config.appConfig.onAppMainLoopFn(e, now.Sub(lastTick)) - lastTick = now - } - } -} - -func bootstrapRunLoopNoDelay(config *bootstrapConfig, e europi.Hardware) { - if config.appConfig.onAppMainLoopFn == nil { - panic(errors.New("no main loop specified")) - } - - lastTick := time.Now() - for { - select { - case reason := <-e.Context().Done(): - panic(reason) - - default: - now := time.Now() - config.appConfig.onAppMainLoopFn(e, now.Sub(lastTick)) - lastTick = now - } - } -} - -func bootstrapDestroy(config *bootstrapConfig, e europi.Hardware, nonPicoWSApi NonPicoWSActivation, reason any) { - if config.onBeginDestroyFn != nil { - config.onBeginDestroyFn(e, reason) - } - - disableUI(e) - - if config.enableNonPicoWebSocket && deactivateNonPicoWebSocket != nil { - deactivateNonPicoWebSocket(e, nonPicoWSApi) - } - - disableDisplayLogger(e) - - uninitRandom(e) - - if display := europi.Display(e); display != nil { - // show the last buffer - _ = display.Display() - } - - if config.onFinishDestroyFn != nil { - config.onFinishDestroyFn(e) - } -} diff --git a/bootstrap/features.go b/bootstrap/features.go deleted file mode 100644 index 72287f9..0000000 --- a/bootstrap/features.go +++ /dev/null @@ -1,71 +0,0 @@ -package bootstrap - -import ( - "context" - "log" - "os" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/experimental/displaylogger" - "github.com/awonak/EuroPiGo/hardware/hal" -) - -var ( - dispLog displaylogger.Logger -) - -func enableDisplayLogger(e europi.Hardware) { - if dispLog != nil { - // already enabled - can happen when panicking - return - } - - display := europi.Display(e) - if display == nil { - // no display, can't continue - return - } - - log.SetFlags(0) - dispLog = displaylogger.NewLogger(display) - log.SetOutput(dispLog) -} - -func disableDisplayLogger(e europi.Hardware) { - flushDisplayLogger(e) - dispLog = nil - log.SetOutput(os.Stdout) -} - -func flushDisplayLogger(e europi.Hardware) { - if dispLog != nil { - dispLog.Flush() - } -} - -func initRandom(e europi.Hardware) { - if rnd := e.Random(); rnd != nil { - _ = rnd.Configure(hal.RandomGeneratorConfig{}) - } -} - -func uninitRandom(e europi.Hardware) { -} - -// used for non-pico testing of bootstrapped europi apps -var ( - defaultWebSimEnabled bool - activateNonPicoWebSocket func(ctx context.Context, e europi.Hardware) NonPicoWSActivation - deactivateNonPicoWebSocket func(e europi.Hardware, api NonPicoWSActivation) -) - -type NonPicoWSActivation interface { - Shutdown() error -} - -func ActivateNonPicoWS(ctx context.Context, e europi.Hardware) NonPicoWSActivation { - if activateNonPicoWebSocket == nil { - return nil - } - return activateNonPicoWebSocket(ctx, e) -} diff --git a/bootstrap/lifecycle.go b/bootstrap/lifecycle.go deleted file mode 100644 index b40667b..0000000 --- a/bootstrap/lifecycle.go +++ /dev/null @@ -1,37 +0,0 @@ -package bootstrap - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" -) - -func DefaultPostBootstrapInitialization(e europi.Hardware) { - display := europi.Display(e) - if display == nil { - // no display, can't continue - return - } - - display.ClearBuffer() - if err := display.Display(); err != nil { - panic(err) - } -} - -func DefaultBootstrapCompleted(e europi.Hardware) { - display := europi.Display(e) - if display == nil { - // no display, can't continue - return - } - - display.ClearBuffer() - if err := display.Display(); err != nil { - panic(err) - } -} - -// DefaultMainLoop is the default main loop used if a new one is not specified to Bootstrap() -func DefaultMainLoop(e europi.Hardware, deltaTime time.Duration) { -} diff --git a/bootstrap/nonpico.go b/bootstrap/nonpico.go deleted file mode 100644 index a31508d..0000000 --- a/bootstrap/nonpico.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build !pico -// +build !pico - -package bootstrap - -import ( - "context" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/internal/nonpico" -) - -func nonPicoActivateWebSocket(ctx context.Context, e europi.Hardware) NonPicoWSActivation { - nonPicoWSApi := nonpico.ActivateWebSocket(ctx, e.Revision()) - return nonPicoWSApi -} - -func nonPicoDeactivateWebSocket(e europi.Hardware, nonPicoWSApi NonPicoWSActivation) { - if nonPicoWSApi != nil { - if err := nonPicoWSApi.Shutdown(); err != nil { - panic(err) - } - } -} - -func init() { - activateNonPicoWebSocket = nonPicoActivateWebSocket - deactivateNonPicoWebSocket = nonPicoDeactivateWebSocket - -} diff --git a/bootstrap/nonpico_panic.go b/bootstrap/nonpico_panic.go deleted file mode 100644 index c9d4f53..0000000 --- a/bootstrap/nonpico_panic.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !pico -// +build !pico - -package bootstrap - -func init() { - DefaultPanicHandler = handlePanicLogger -} diff --git a/bootstrap/nonpico_websimenabled.go b/bootstrap/nonpico_websimenabled.go deleted file mode 100644 index ead2b1d..0000000 --- a/bootstrap/nonpico_websimenabled.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !pico && websim -// +build !pico,websim - -package bootstrap - -func init() { - defaultWebSimEnabled = true -} diff --git a/bootstrap/options.go b/bootstrap/options.go deleted file mode 100644 index 437609c..0000000 --- a/bootstrap/options.go +++ /dev/null @@ -1,28 +0,0 @@ -package bootstrap - -import europi "github.com/awonak/EuroPiGo" - -// BootstrapOption is a single configuration parameter passed to the Bootstrap() function -type BootstrapOption func(o *bootstrapConfig) error - -type bootstrapConfig struct { - panicHandler func(e europi.Hardware, reason any) - enableDisplayLogger bool - initRandom bool - europi europi.Hardware - enableNonPicoWebSocket bool - - // application - appConfig bootstrapAppConfig - - // user interface - uiConfig bootstrapUIConfig - - // lifecycle callbacks - onPostBootstrapConstructionFn PostBootstrapConstructionFunc - onPreInitializeComponentsFn PreInitializeComponentsFunc - onPostInitializeComponentsFn PostInitializeComponentsFunc - onBootstrapCompletedFn BootstrapCompletedFunc - onBeginDestroyFn BeginDestroyFunc - onFinishDestroyFn FinishDestroyFunc -} diff --git a/bootstrap/options_app.go b/bootstrap/options_app.go deleted file mode 100644 index 95fd7a1..0000000 --- a/bootstrap/options_app.go +++ /dev/null @@ -1,100 +0,0 @@ -package bootstrap - -import ( - "errors" - "time" - - europi "github.com/awonak/EuroPiGo" -) - -type ApplicationStart[THardware europi.Hardware] interface { - Start(e THardware) -} - -type ApplicationMainLoop[THardware europi.Hardware] interface { - MainLoop(e THardware, deltaTime time.Duration) -} - -type ApplicationEnd[THardware europi.Hardware] interface { - End(e THardware) -} - -// App sets the application handler interface with optional parameters -func App(app any, opts ...BootstrapAppOption) BootstrapOption { - return func(o *bootstrapConfig) error { - if app == nil { - return errors.New("app must not be nil") - } - - // automatically divine the functions for the app - start, mainLoop, end := getAppFuncs(o.europi, app) - - if start == nil && mainLoop == nil && end == nil { - return errors.New("app must provide at least one application function interface (ApplicationStart, ApplicationMainLoop, ApplicationEnd)") - } - - o.appConfig.onAppStartFn = start - o.appConfig.onAppMainLoopFn = mainLoop - o.appConfig.onAppEndFn = end - - o.appConfig.options = opts - return nil - } -} - -// AppOptions adds optional parameters for setting up the application interface -func AppOptions(option BootstrapAppOption, opts ...BootstrapAppOption) BootstrapOption { - return func(o *bootstrapConfig) error { - o.appConfig.options = append(o.appConfig.options, opts...) - return nil - } -} - -// BootstrapAppOption is a single configuration parameter passed to the App() or AppOption() functions -type BootstrapAppOption func(o *bootstrapAppConfig) error - -type bootstrapAppConfig struct { - mainLoopInterval time.Duration - onAppStartFn AppStartFunc - onAppMainLoopFn AppMainLoopFunc - onAppEndFn AppEndFunc - - options []BootstrapAppOption -} - -const ( - DefaultAppMainLoopInterval time.Duration = time.Millisecond * 100 -) - -// AppMainLoopInterval sets the interval between calls to the configured app main loop function -func AppMainLoopInterval(interval time.Duration) BootstrapAppOption { - return func(o *bootstrapAppConfig) error { - if interval < 0 { - return errors.New("interval must be greater than or equal to 0") - } - o.mainLoopInterval = interval - return nil - } -} - -func getAppFuncs(e europi.Hardware, app any) (start AppStartFunc, mainLoop AppMainLoopFunc, end AppEndFunc) { - if appStart, _ := app.(ApplicationStart[europi.Hardware]); appStart != nil { - start = appStart.Start - } - if appMainLoop, _ := app.(ApplicationMainLoop[europi.Hardware]); appMainLoop != nil { - mainLoop = appMainLoop.MainLoop - } - if appEnd, _ := app.(ApplicationEnd[europi.Hardware]); appEnd != nil { - end = appEnd.End - } - - switch e.(type) { - case *europi.EuroPiPrototype: - start, mainLoop, end = getWrappedAppFuncs[*europi.EuroPiPrototype](app) - case *europi.EuroPi: - start, mainLoop, end = getWrappedAppFuncs[*europi.EuroPi](app) - // TODO: add rev2 - } - - return -} diff --git a/bootstrap/options_features.go b/bootstrap/options_features.go deleted file mode 100644 index ea6fc4b..0000000 --- a/bootstrap/options_features.go +++ /dev/null @@ -1,40 +0,0 @@ -package bootstrap - -const ( - DefaultEnableDisplayLogger bool = false -) - -// EnableDisplayLogger enables (or disables) the logging of `log.Printf` (and similar) messages to -// the EuroPi's display. Enabling this will likely be undesirable except in cases where on-screen -// debugging is absoluely necessary. -func EnableDisplayLogger(enabled bool) BootstrapOption { - return func(o *bootstrapConfig) error { - o.enableDisplayLogger = enabled - return nil - } -} - -const ( - DefaultInitRandom bool = true -) - -// InitRandom enables (or disables) the initialization of the Go standard library's `rand` package -// Seed value. Disabling this will likely be undesirable except in cases where deterministic 'random' -// number generation is required, as the standard library `rand` package defaults to a seed of 1 -// instead of some pseudo-random number, like current time or thermal values. -// To generate a pseudo-random number for the random seed, the `machine.GetRNG` function is used. -func InitRandom(enabled bool) BootstrapOption { - return func(o *bootstrapConfig) error { - o.initRandom = enabled - return nil - } -} - -// AttachNonPicoWS (if enabled and on non-Pico builds with build flags of `-tags=revision1` set) -// starts up a websocket interface and system debugger on port 8080 -func AttachNonPicoWS(enabled bool) BootstrapOption { - return func(o *bootstrapConfig) error { - o.enableNonPicoWebSocket = enabled - return nil - } -} diff --git a/bootstrap/options_lifecycle.go b/bootstrap/options_lifecycle.go deleted file mode 100644 index 52e8217..0000000 --- a/bootstrap/options_lifecycle.go +++ /dev/null @@ -1,161 +0,0 @@ -package bootstrap - -import ( - "errors" - "time" - - europi "github.com/awonak/EuroPiGo" -) - -/* Order of lifecycle calls: -BootStrap - | - V -Callback: PostBootstrapConstruction - | - V -Bootstrap: postBootstrapConstruction - | - V - Callback: PreInitializeComponents - | - V - Bootstrap: initializeComponents - | - V - Callback: PostInitializeComponents - | - V -Callback: BootstrapCompleted - | - V -Bootstrap: runLoop - | - V - Callback: AppStart - | - V - Callback(on tick): AppMainLoop - | - V - Callback: AppEnd - | - V -Bootstrap: destroyBootstrap - | - V - Callback: BeginDestroy - | - V - Callback: FinishDestroy -*/ - -type ( - PostBootstrapConstructionFunc func(e europi.Hardware) - PreInitializeComponentsFunc func(e europi.Hardware) - PostInitializeComponentsFunc func(e europi.Hardware) - BootstrapCompletedFunc func(e europi.Hardware) - AppStartFunc func(e europi.Hardware) - AppMainLoopFunc func(e europi.Hardware, deltaTime time.Duration) - AppEndFunc func(e europi.Hardware) - BeginDestroyFunc func(e europi.Hardware, reason any) - FinishDestroyFunc func(e europi.Hardware) -) - -// PostBootstrapConstruction sets the function that runs immediately after primary EuroPi bootstrap -// has finished, but before components have been initialized. Nearly none of the functionality of -// the bootstrap is ready or configured at this point. -func PostBootstrapConstruction(fn PostBootstrapConstructionFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - o.onPostBootstrapConstructionFn = fn - return nil - } -} - -// PreInitializeComponents sets the function that recevies notification of when components of the -// bootstrap are about to start their initialization phase and the bootstrap is getting ready. -// Most operational functionality of the bootstrap is definitely not configured at this point. -func PreInitializeComponents(fn PreInitializeComponentsFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - o.onPreInitializeComponentsFn = fn - return nil - } -} - -// PostInitializeComponents sets the function that recevies notification of when components of the -// bootstrap have completed their initialization phase and the bootstrap is nearly ready for full -// operation. Some operational functionality of the bootstrap might not be configured at this point. -func PostInitializeComponents(fn PostInitializeComponentsFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - o.onPostInitializeComponentsFn = fn - return nil - } -} - -// BootstrapCompleted sets the function that receives notification of critical bootstrap -// operations being complete - this is the first point where functions within the bootstrap -// may be used without fear of there being an incomplete operating state. -func BootstrapCompleted(fn BootstrapCompletedFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - o.onBootstrapCompletedFn = fn - return nil - } -} - -// TODO: consider secondary bootloader support functionality here once internal flash support -// becomes a reality. - -// AppStart sets the application function to be called before the main operating loop -// processing begins. At this point, the bootstrap configuration has completed and -// all bootstrap functionality may be used without fear of there being an incomplete -// operating state. -func AppStart(fn AppStartFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - o.appConfig.onAppStartFn = fn - return nil - } -} - -// AppMainLoop sets the application main loop function to be called on interval. -// nil is not allowed - if you want to set the default, either do not specify a AppMainLoop() option -// or specify europi.DefaultMainLoop -func AppMainLoop(fn AppMainLoopFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - if fn == nil { - return errors.New("a valid main loop function must be specified") - } - o.appConfig.onAppMainLoopFn = fn - return nil - } -} - -// AppEnd sets the application function that's called right before the bootstrap -// destruction processing is performed. -func AppEnd(fn AppEndFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - o.appConfig.onAppEndFn = fn - return nil - } -} - -// BeginDestroy sets the function that receives the notification of shutdown of the bootstrap and -// is also the first stop within the `panic()` handler functionality. If the `reason` parameter -// is non-nil, then a critical failure has been detected and the bootstrap is in the last stages of -// complete destruction. If it is nil, then it can be assumed that proper functionality of the -// bootstrap is still available, but heading towards the last steps of unavailability once the -// function exits. -func BeginDestroy(fn BeginDestroyFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - o.onBeginDestroyFn = fn - return nil - } -} - -// FinishDestroy sets the function that receives the final notification of shutdown of the bootstrap. -// The entire bootstrap is disabled, all timers, queues, and components are considered deactivated. -func FinishDestroy(fn FinishDestroyFunc) BootstrapOption { - return func(o *bootstrapConfig) error { - o.onFinishDestroyFn = fn - return nil - } -} diff --git a/bootstrap/options_ui.go b/bootstrap/options_ui.go deleted file mode 100644 index 3ae52cd..0000000 --- a/bootstrap/options_ui.go +++ /dev/null @@ -1,53 +0,0 @@ -package bootstrap - -import ( - "errors" - "time" - - europi "github.com/awonak/EuroPiGo" -) - -// UI sets the user interface handler interface -func UI(ui UserInterface[europi.Hardware], opts ...BootstrapUIOption) BootstrapOption { - return func(o *bootstrapConfig) error { - if ui == nil { - return errors.New("ui must not be nil") - } - o.uiConfig.ui = ui - o.uiConfig.options = opts - return nil - } -} - -const ( - DefaultUIRefreshRate time.Duration = time.Millisecond * 100 -) - -// BootstrapOption is a single configuration parameter passed to the Bootstrap() function -type BootstrapUIOption func(o *bootstrapUIConfig) error - -type bootstrapUIConfig struct { - ui UserInterface[europi.Hardware] - uiRefreshRate time.Duration - - options []BootstrapUIOption -} - -// UIOptions adds optional parameters for setting up the user interface -func UIOptions(option BootstrapUIOption, opts ...BootstrapUIOption) BootstrapOption { - return func(o *bootstrapConfig) error { - o.uiConfig.options = append(o.uiConfig.options, opts...) - return nil - } -} - -// UIRefreshRate sets the interval of refreshes of the user interface -func UIRefreshRate(interval time.Duration) BootstrapUIOption { - return func(o *bootstrapUIConfig) error { - if interval <= 0 { - return errors.New("interval must be greater than 0") - } - o.uiRefreshRate = interval - return nil - } -} diff --git a/bootstrap/panic.go b/bootstrap/panic.go deleted file mode 100644 index 6e65d52..0000000 --- a/bootstrap/panic.go +++ /dev/null @@ -1,62 +0,0 @@ -package bootstrap - -import ( - "fmt" - "log" - "os" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/experimental/draw" - "tinygo.org/x/tinydraw" -) - -// DefaultPanicHandler is the default handler for panics -// This will be set by the build flag `onscreenpanic` to `handlePanicOnScreenLog` -// Not setting the build flag will set it to `handlePanicDisplayCrash` -var DefaultPanicHandler func(e europi.Hardware, reason any) - -var ( - // silence linter - _, _ = handlePanicOnScreenLog, handlePanicDisplayCrash -) - -func handlePanicOnScreenLog(e europi.Hardware, reason any) { - if e == nil { - // can't do anything if it's not enabled - return - } - - // force display-logging to enabled - enableDisplayLogger(e) - - // show the panic on the screen - log.Println(fmt.Sprint(reason)) - - flushDisplayLogger(e) - - os.Exit(1) -} - -func handlePanicLogger(e europi.Hardware, reason any) { - log.Panic(reason) -} - -func handlePanicDisplayCrash(e europi.Hardware, reason any) { - display := europi.Display(e) - if display == nil { - // can't do anything if we don't have a display - return - } - - // display a diagonal line pattern through the screen to show that the EuroPi is crashed - width, height := display.Size() - ymax := height - 1 - for x := -ymax; x < width; x += 4 { - lx, ly := x, int16(0) - if x < 0 { - lx = 0 - ly = -x - } - tinydraw.Line(display, lx, ly, x+ymax, ymax, draw.White) - } -} diff --git a/bootstrap/pico_panicdisabled.go b/bootstrap/pico_panicdisabled.go deleted file mode 100644 index 4a1aa5c..0000000 --- a/bootstrap/pico_panicdisabled.go +++ /dev/null @@ -1,20 +0,0 @@ -//go:build pico && !onscreenpanic -// +build pico,!onscreenpanic - -package bootstrap - -import ( - "github.com/awonak/EuroPiGo/hardware" - "github.com/awonak/EuroPiGo/hardware/hal" -) - -func init() { - hardware.OnRevisionDetected(func(revision hal.Revision) { - switch revision { - case hal.RevisionUnknown, hal.EuroPiProto: - DefaultPanicHandler = handlePanicLogger - default: - DefaultPanicHandler = handlePanicDisplayCrash - } - }) -} diff --git a/bootstrap/pico_panicenabled.go b/bootstrap/pico_panicenabled.go deleted file mode 100644 index 1b652ca..0000000 --- a/bootstrap/pico_panicenabled.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build pico && onscreenpanic -// +build pico,onscreenpanic - -package bootstrap - -func init() { - DefaultPanicHandler = handlePanicOnScreenLog -} diff --git a/bootstrap/ui.go b/bootstrap/ui.go deleted file mode 100644 index f5a9e5b..0000000 --- a/bootstrap/ui.go +++ /dev/null @@ -1,76 +0,0 @@ -package bootstrap - -import ( - "context" - "time" - - europi "github.com/awonak/EuroPiGo" -) - -type UserInterface[THardware europi.Hardware] interface { - Start(e THardware) - Paint(e THardware, deltaTime time.Duration) -} - -type UserInterfaceLogoPainter[THardware europi.Hardware] interface { - PaintLogo(e THardware, deltaTime time.Duration) -} - -type UserInterfaceButton1[THardware europi.Hardware] interface { - Button1(e THardware, deltaTime time.Duration) -} - -type UserInterfaceButton1Debounce interface { - Button1Debounce() time.Duration -} - -type UserInterfaceButton1Ex[THardware europi.Hardware] interface { - Button1Ex(e THardware, value bool, deltaTime time.Duration) -} - -type UserInterfaceButton1Long[THardware europi.Hardware] interface { - Button1Long(e THardware, deltaTime time.Duration) -} - -type UserInterfaceButton2[THardware europi.Hardware] interface { - Button2(e THardware, deltaTime time.Duration) -} - -type UserInterfaceButton2Debounce interface { - Button2Debounce() time.Duration -} - -type UserInterfaceButton2Ex[THardware europi.Hardware] interface { - Button2Ex(e THardware, value bool, deltaTime time.Duration) -} - -type UserInterfaceButton2Long[THardware europi.Hardware] interface { - Button2Long(e THardware, deltaTime time.Duration) -} - -var ( - ui uiModule -) - -func enableUI(ctx context.Context, e europi.Hardware, config bootstrapUIConfig) { - ui.setup(e, config.ui) - - ui.start(ctx, e, config.uiRefreshRate) -} - -func startUI(e europi.Hardware) { - if ui.screen == nil { - return - } - - ui.screen.Start(e) -} - -// ForceRepaintUI schedules a forced repaint of the UI (if it is configured and running) -func ForceRepaintUI(e europi.Hardware) { - ui.repaint() -} - -func disableUI(e europi.Hardware) { - ui.shutdown() -} diff --git a/bootstrap/uimodule.go b/bootstrap/uimodule.go deleted file mode 100644 index 16c6e6f..0000000 --- a/bootstrap/uimodule.go +++ /dev/null @@ -1,193 +0,0 @@ -package bootstrap - -import ( - "context" - "sync" - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/debounce" - "github.com/awonak/EuroPiGo/hardware/hal" -) - -// LongPressDuration is the amount of time a button is in a held/pressed state before -// it is considered to be a 'long' press. -// TODO: This is eventually intended to be a persisted setting, configurable by the user. -const LongPressDuration = time.Millisecond * 650 - -type uiModule struct { - screen UserInterface[europi.Hardware] - logoPainter UserInterfaceLogoPainter[europi.Hardware] - repaintCh chan struct{} - stop context.CancelFunc - wg sync.WaitGroup -} - -func (u *uiModule) setup(e europi.Hardware, screen UserInterface[europi.Hardware]) { - b1 := europi.Button(e, 0) - b2 := europi.Button(e, 1) - - ui.screen = screen - if ui.screen == nil { - return - } - - ui.logoPainter, _ = screen.(UserInterfaceLogoPainter[europi.Hardware]) - - ui.repaintCh = make(chan struct{}, 1) - - var ( - inputB1 func(e europi.Hardware, value bool, deltaTime time.Duration) - inputB1L func(e europi.Hardware, deltaTime time.Duration) - ) - if in, ok := screen.(UserInterfaceButton1[europi.Hardware]); ok { - var debounceDelay time.Duration - if db, ok := screen.(UserInterfaceButton1Debounce); ok { - debounceDelay = db.Button1Debounce() - } - inputDB := debounce.NewDebouncer(func(value bool, deltaTime time.Duration) { - if !value { - in.Button1(e, deltaTime) - } - }).Debounce(debounceDelay) - inputB1 = func(e europi.Hardware, value bool, deltaTime time.Duration) { - inputDB(value) - } - } else if in, ok := screen.(UserInterfaceButton1Ex[europi.Hardware]); ok { - inputB1 = in.Button1Ex - } - if in, ok := screen.(UserInterfaceButton1Long[europi.Hardware]); ok { - inputB1L = in.Button1Long - } - ui.setupButton(e, b1, inputB1, inputB1L) - - var ( - inputB2 func(e europi.Hardware, value bool, deltaTime time.Duration) - inputB2L func(e europi.Hardware, deltaTime time.Duration) - ) - if in, ok := screen.(UserInterfaceButton2[europi.Hardware]); ok { - var debounceDelay time.Duration - if db, ok := screen.(UserInterfaceButton2Debounce); ok { - debounceDelay = db.Button2Debounce() - } - inputDB := debounce.NewDebouncer(func(value bool, deltaTime time.Duration) { - if !value { - in.Button2(e, deltaTime) - } - }).Debounce(debounceDelay) - inputB2 = func(e europi.Hardware, value bool, deltaTime time.Duration) { - inputDB(value) - } - } else if in, ok := screen.(UserInterfaceButton2Ex[europi.Hardware]); ok { - inputB2 = in.Button2Ex - } - if in, ok := screen.(UserInterfaceButton2Long[europi.Hardware]); ok { - inputB2L = in.Button2Long - } - ui.setupButton(e, b2, inputB2, inputB2L) -} - -func (u *uiModule) start(ctx context.Context, e europi.Hardware, interval time.Duration) { - ui.wg.Add(1) - go ui.run(ctx, e, interval) -} - -func (u *uiModule) wait() { - u.wg.Wait() -} - -func (u *uiModule) repaint() { - if u.repaintCh != nil { - u.repaintCh <- struct{}{} - } -} - -func (u *uiModule) shutdown() { - if u.stop != nil { - u.stop() - } - - if ui.repaintCh != nil { - close(ui.repaintCh) - } - - ui.wait() -} - -func (u *uiModule) run(ctx context.Context, e europi.Hardware, interval time.Duration) { - defer u.wg.Done() - - disp := europi.Display(e) - if disp == nil { - // no display means no ui - // TODO: make uiModule work when any user input/output is specified, not just display - return - } - - myCtx, cancel := context.WithCancel(ctx) - ui.stop = cancel - defer ui.stop() - - t := time.NewTicker(interval) - defer t.Stop() - - paint := func(deltaTime time.Duration) { - disp.ClearBuffer() - if u.logoPainter != nil { - u.logoPainter.PaintLogo(e, deltaTime) - } - u.screen.Paint(e, deltaTime) - _ = disp.Display() - } - - lastTime := time.Now() - for { - select { - case <-myCtx.Done(): - return - - case <-ui.repaintCh: - now := time.Now() - deltaTime := now.Sub(lastTime) - lastTime = now - paint(deltaTime) - - case now := <-t.C: - deltaTime := now.Sub(lastTime) - lastTime = now - paint(deltaTime) - } - } -} - -func (u *uiModule) setupButton(e europi.Hardware, btn hal.ButtonInput, onShort func(e europi.Hardware, value bool, deltaTime time.Duration), onLong func(e europi.Hardware, deltaTime time.Duration)) { - if btn == nil { - return - } - - if onShort == nil && onLong == nil { - return - } - - if onShort == nil { - // no-op - onShort = func(e europi.Hardware, value bool, deltaTime time.Duration) {} - } - - // if no long-press handler present, just reuse short-press handler - if onLong == nil { - onLong = func(e europi.Hardware, deltaTime time.Duration) { - onShort(e, false, deltaTime) - } - } - - btn.HandlerEx(hal.ChangeAny, func(value bool, deltaTime time.Duration) { - if value { - onShort(e, value, deltaTime) - } else if deltaTime < LongPressDuration { - onShort(e, value, deltaTime) - } else { - onLong(e, deltaTime) - } - }) -} diff --git a/debug.go b/debug.go index 6784acd..18294e5 100644 --- a/debug.go +++ b/debug.go @@ -1,6 +1,7 @@ package europi import ( + "context" "log" "runtime" "time" @@ -29,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..dc3bf0b --- /dev/null +++ b/debug_nonpico.go @@ -0,0 +1,19 @@ +//go:build !pico +// +build !pico + +package bootstrap + +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/experimental/knobmenu/item.go b/experimental/knobmenu/item.go deleted file mode 100644 index 0ce68d7..0000000 --- a/experimental/knobmenu/item.go +++ /dev/null @@ -1,10 +0,0 @@ -package knobmenu - -import "github.com/awonak/EuroPiGo/units" - -type item struct { - name string - label string - stringFn func() string - updateFn func(value units.CV) -} diff --git a/experimental/knobmenu/knobmenu.go b/experimental/knobmenu/knobmenu.go deleted file mode 100644 index b2621da..0000000 --- a/experimental/knobmenu/knobmenu.go +++ /dev/null @@ -1,107 +0,0 @@ -package knobmenu - -import ( - "fmt" - "time" - - 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/experimental/knobbank" - "github.com/awonak/EuroPiGo/hardware/hal" - "tinygo.org/x/tinyfont/proggy" -) - -var ( - DefaultFont = &proggy.TinySZ8pt7b -) - -type KnobMenu struct { - kb *knobbank.KnobBank - items []item - selectedRune rune - unselectedRune rune - x int16 - y int16 - yadvance int16 - writer fontwriter.Writer -} - -func NewKnobMenu(knob hal.KnobInput, opts ...KnobMenuOption) (*KnobMenu, error) { - km := &KnobMenu{ - selectedRune: '*', - unselectedRune: ' ', - x: 0, - y: 11, - yadvance: 12, - writer: fontwriter.Writer{ - Display: nil, - Font: DefaultFont, - }, - } - - km.yadvance = int16(km.writer.Font.GetYAdvance()) - km.y = km.yadvance - - kbopts := []knobbank.KnobBankOption{ - knobbank.WithDisabledKnob(), - } - - for _, opt := range opts { - kbo, err := opt(km) - if err != nil { - return nil, err - } - - kbopts = append(kbopts, kbo...) - } - - kb, err := knobbank.NewKnobBank(knob, kbopts...) - if err != nil { - return nil, err - } - - km.kb = kb - - return km, nil -} - -func (m *KnobMenu) Next() { - m.kb.Next() -} - -func (m *KnobMenu) Paint(e europi.Hardware, deltaTime time.Duration) { - m.updateMenu(e) - - m.writer.Display = europi.Display(e) - if m.writer.Display == nil { - return - } - - y := m.y - selectedIdx := m.kb.CurrentIndex() - 1 - minI := clamp.Clamp(selectedIdx-1, 0, len(m.items)-1) - maxI := clamp.Clamp(minI+1, 0, len(m.items)-1) - for i := minI; i <= maxI && i < len(m.items); i++ { - it := &m.items[i] - - selRune := m.unselectedRune - if i == selectedIdx { - selRune = m.selectedRune - } - - m.writer.WriteLine(fmt.Sprintf("%c%s:%s", selRune, it.label, it.stringFn()), m.x, y, draw.White) - y += m.yadvance - } -} - -func (m *KnobMenu) updateMenu(e europi.Hardware) { - cur := m.kb.CurrentName() - for _, it := range m.items { - if it.name == cur { - it.updateFn(m.kb.ReadCV()) - return - } - } -} diff --git a/experimental/knobmenu/options.go b/experimental/knobmenu/options.go deleted file mode 100644 index db55911..0000000 --- a/experimental/knobmenu/options.go +++ /dev/null @@ -1,54 +0,0 @@ -package knobmenu - -import ( - "fmt" - - "github.com/awonak/EuroPiGo/experimental/knobbank" - "github.com/awonak/EuroPiGo/units" - "tinygo.org/x/tinyfont" -) - -type KnobMenuOption func(km *KnobMenu) ([]knobbank.KnobBankOption, error) - -func WithItem(name, label string, stringFn func() string, valueFn func() units.CV, updateFn func(value units.CV)) KnobMenuOption { - return func(km *KnobMenu) ([]knobbank.KnobBankOption, error) { - for _, it := range km.items { - if it.name == name { - return nil, fmt.Errorf("item %q already exists", name) - } - } - - km.items = append(km.items, item{ - name: name, - label: label, - stringFn: stringFn, - updateFn: updateFn, - }) - - return []knobbank.KnobBankOption{ - knobbank.WithLockedKnob(name, knobbank.InitialPercentageValue(valueFn().ToFloat32())), - }, nil - } -} - -func WithPosition(x, y int16) KnobMenuOption { - return func(km *KnobMenu) ([]knobbank.KnobBankOption, error) { - km.x = x - km.y = y - return nil, nil - } -} - -func WithYAdvance(yadvance int16) KnobMenuOption { - return func(km *KnobMenu) ([]knobbank.KnobBankOption, error) { - km.yadvance = yadvance - return nil, nil - } -} - -func WithFont(font tinyfont.Fonter) KnobMenuOption { - return func(km *KnobMenu) ([]knobbank.KnobBankOption, error) { - km.writer.Font = font - return nil, nil - } -} diff --git a/experimental/screenbank/entry.go b/experimental/screenbank/entry.go deleted file mode 100644 index c65e2f2..0000000 --- a/experimental/screenbank/entry.go +++ /dev/null @@ -1,32 +0,0 @@ -package screenbank - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" -) - -type entry struct { - name string - logo string - screen entryWrapper[europi.Hardware] - enabled bool - locked bool - lastUpdate time.Time -} - -func (e *entry) lock() { - if e.locked { - return - } - - e.locked = true -} - -func (e *entry) unlock() { - if !e.enabled { - return - } - - e.locked = false -} diff --git a/experimental/screenbank/entrywrapper.go b/experimental/screenbank/entrywrapper.go deleted file mode 100644 index dbe3f06..0000000 --- a/experimental/screenbank/entrywrapper.go +++ /dev/null @@ -1,73 +0,0 @@ -package screenbank - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/bootstrap" -) - -type entryWrapper[THardware europi.Hardware] struct { - screen bootstrap.UserInterface[THardware] - button1 bootstrap.UserInterfaceButton1[THardware] - button1Long bootstrap.UserInterfaceButton1Long[THardware] - button1Ex bootstrap.UserInterfaceButton1Ex[THardware] - button2 bootstrap.UserInterfaceButton2[THardware] - button2Ex bootstrap.UserInterfaceButton2Ex[THardware] -} - -func (w *entryWrapper[THardware]) Start(e europi.Hardware) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.screen.Start(pi) -} - -func (w *entryWrapper[THardware]) Paint(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.screen.Paint(pi, deltaTime) -} - -func (w *entryWrapper[THardware]) Button1(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button1.Button1(pi, deltaTime) -} - -func (w *entryWrapper[THardware]) Button1Ex(e europi.Hardware, value bool, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button1Ex.Button1Ex(pi, value, deltaTime) -} - -func (w *entryWrapper[THardware]) Button1Long(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button1Long.Button1Long(pi, deltaTime) -} - -func (w *entryWrapper[THardware]) Button2(e europi.Hardware, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button2.Button2(pi, deltaTime) -} - -func (w *entryWrapper[THardware]) Button2Ex(e europi.Hardware, value bool, deltaTime time.Duration) { - pi, ok := e.(THardware) - if !ok { - panic("incorrect hardware type conversion") - } - w.button2Ex.Button2Ex(pi, value, deltaTime) -} diff --git a/experimental/screenbank/options.go b/experimental/screenbank/options.go deleted file mode 100644 index fb5e338..0000000 --- a/experimental/screenbank/options.go +++ /dev/null @@ -1,59 +0,0 @@ -package screenbank - -import ( - "fmt" - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/bootstrap" -) - -type ScreenBankOption func(sb *ScreenBank) error - -// WithScreen sets up a new screen in the chain -// logo is the emoji to use (see https://github.com/tinygo-org/tinyfont/blob/release/notoemoji/NotoEmoji-Regular-12pt.go) -func WithScreen(name string, logo string, screen any) ScreenBankOption { - return func(sb *ScreenBank) error { - details, ok := getScreen(screen) - if !ok { - return fmt.Errorf("screen %q does not implement a variant of bootstrap.UserInterface", name) - } - e := entry{ - name: name, - logo: logo, - screen: details, - enabled: true, - locked: true, - lastUpdate: time.Now(), - } - - sb.bank = append(sb.bank, e) - return nil - } -} - -func getScreen(screen any) (details entryWrapper[europi.Hardware], ok bool) { - if s, _ := screen.(bootstrap.UserInterface[europi.Hardware]); s != nil { - details.screen = s - details.button1, _ = screen.(bootstrap.UserInterfaceButton1[europi.Hardware]) - details.button1Long, _ = screen.(bootstrap.UserInterfaceButton1Long[europi.Hardware]) - details.button1Ex, _ = screen.(bootstrap.UserInterfaceButton1Ex[europi.Hardware]) - details.button2, _ = screen.(bootstrap.UserInterfaceButton2[europi.Hardware]) - details.button2Ex, _ = screen.(bootstrap.UserInterfaceButton2Ex[europi.Hardware]) - - ok = true - return - } - - if details, ok = getScreenForHardware[*europi.EuroPiPrototype](screen); ok { - return - } - - if details, ok = getScreenForHardware[*europi.EuroPi](screen); ok { - return - } - - // TODO: add rev2 - - return -} diff --git a/experimental/screenbank/screen_conversion.go b/experimental/screenbank/screen_conversion.go deleted file mode 100644 index 1a90805..0000000 --- a/experimental/screenbank/screen_conversion.go +++ /dev/null @@ -1,38 +0,0 @@ -package screenbank - -import ( - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/bootstrap" -) - -func getScreenForHardware[THardware europi.Hardware](screen any) (details entryWrapper[europi.Hardware], ok bool) { - s, _ := screen.(bootstrap.UserInterface[THardware]) - if s == nil { - return - } - - wrapper := &entryWrapper[THardware]{ - screen: s, - } - - details.screen = wrapper - - if wrapper.button1, _ = screen.(bootstrap.UserInterfaceButton1[THardware]); wrapper.button1 != nil { - details.button1 = wrapper - } - if wrapper.button1Long, _ = screen.(bootstrap.UserInterfaceButton1Long[THardware]); wrapper.button1Long != nil { - details.button1Long = wrapper - } - if wrapper.button1Ex, _ = screen.(bootstrap.UserInterfaceButton1Ex[THardware]); wrapper.button1Ex != nil { - details.button1Ex = wrapper - } - if wrapper.button2, _ = screen.(bootstrap.UserInterfaceButton2[THardware]); wrapper.button2 != nil { - details.button2 = wrapper - } - if wrapper.button2Ex, _ = screen.(bootstrap.UserInterfaceButton2Ex[THardware]); wrapper.button2Ex != nil { - details.button2Ex = wrapper - } - - ok = true - return -} diff --git a/experimental/screenbank/screenbank.go b/experimental/screenbank/screenbank.go deleted file mode 100644 index 354beeb..0000000 --- a/experimental/screenbank/screenbank.go +++ /dev/null @@ -1,156 +0,0 @@ -package screenbank - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/experimental/draw" - "github.com/awonak/EuroPiGo/experimental/fontwriter" - "tinygo.org/x/tinyfont/notoemoji" -) - -type ScreenBank struct { - current int - bank []entry - writer fontwriter.Writer -} - -var ( - DefaultFont = ¬oemoji.NotoEmojiRegular12pt -) - -func NewScreenBank(opts ...ScreenBankOption) (*ScreenBank, error) { - sb := &ScreenBank{ - writer: fontwriter.Writer{ - Font: DefaultFont, - }, - } - - for _, opt := range opts { - if err := opt(sb); err != nil { - return nil, err - } - } - - return sb, nil -} - -func (sb *ScreenBank) CurrentName() string { - if len(sb.bank) == 0 { - return "" - } - return sb.bank[sb.current].name -} - -func (sb *ScreenBank) Current() *entryWrapper[europi.Hardware] { - if len(sb.bank) == 0 { - return nil - } - return &sb.bank[sb.current].screen -} - -func (sb *ScreenBank) transitionTo(idx int) { - if sb.current >= len(sb.bank) || len(sb.bank) == 0 { - return - } - - cur := sb.bank[sb.current] - cur.lock() - sb.current = idx - if sb.current >= len(sb.bank) { - sb.current = 0 - } - sb.bank[sb.current].unlock() -} - -func (sb *ScreenBank) Goto(idx int) { - sb.transitionTo(idx) -} - -func (sb *ScreenBank) GotoNamed(name string) { - for i, screen := range sb.bank { - if screen.name == name { - sb.transitionTo(i) - return - } - } -} - -func (sb *ScreenBank) Next() { - sb.transitionTo(sb.current + 1) -} - -func (sb *ScreenBank) Start(e europi.Hardware) { - for i := range sb.bank { - s := &sb.bank[i] - - s.lock() - s.screen.screen.Start(e) - s.lastUpdate = time.Now() - s.unlock() - } -} - -func (sb *ScreenBank) PaintLogo(e europi.Hardware, deltaTime time.Duration) { - display := europi.Display(e) - if sb.current >= len(sb.bank) || display == nil { - return - } - - cur := &sb.bank[sb.current] - cur.lock() - if cur.logo != "" { - sb.writer.Display = display - sb.writer.WriteLineInverseAligned(cur.logo, 0, 16, draw.White, fontwriter.AlignRight, fontwriter.AlignMiddle) - } - cur.unlock() -} - -func (sb *ScreenBank) Paint(e europi.Hardware, deltaTime time.Duration) { - if sb.current >= len(sb.bank) { - return - } - - cur := &sb.bank[sb.current] - cur.lock() - now := time.Now() - cur.screen.screen.Paint(e, now.Sub(cur.lastUpdate)) - cur.lastUpdate = now - cur.unlock() -} - -func (sb *ScreenBank) Button1Ex(e europi.Hardware, value bool, deltaTime time.Duration) { - screen := sb.Current() - if cur := screen.button1; cur != nil { - if !value { - cur.Button1(e, deltaTime) - } - } else if cur := screen.button1Ex; cur != nil { - cur.Button1Ex(e, value, deltaTime) - } -} - -func (sb *ScreenBank) Button1Long(e europi.Hardware, deltaTime time.Duration) { - screen := sb.Current() - if cur := screen.button1Long; cur != nil { - cur.Button1Long(e, deltaTime) - } else { - // try the short-press - sb.Button1Ex(e, false, deltaTime) - } -} - -func (sb *ScreenBank) Button2Ex(e europi.Hardware, value bool, deltaTime time.Duration) { - screen := sb.Current() - if cur := screen.button2; cur != nil { - if !value { - cur.Button2(e, deltaTime) - } - } else if cur := screen.button2Ex; cur != nil { - cur.Button2Ex(e, value, deltaTime) - } -} - -func (sb *ScreenBank) Button2Long(e europi.Hardware, deltaTime time.Duration) { - sb.Next() -} diff --git a/internal/projects/clockgenerator/LICENSE.md b/internal/projects/clockgenerator/LICENSE.md deleted file mode 100644 index b7aa0e2..0000000 --- a/internal/projects/clockgenerator/LICENSE.md +++ /dev/null @@ -1,11 +0,0 @@ -# Released under MIT License - -Copyright (c) 2023 Jason Crawford - -Portions Copyright (c) 2022 Adam Wonak - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/internal/projects/clockgenerator/README.md b/internal/projects/clockgenerator/README.md deleted file mode 100644 index 1d67318..0000000 --- a/internal/projects/clockgenerator/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Clock Generator - -A simple gate/clock generator based on YouTube video observations made about the operation of the ALM Pamela's NEW Workout module. - -## Scope of This App - -The scope of this app is to drive the CV 1 output as a gate output. - -### Outputs - -- CV 1 = Gate output - -## Using Clock Generator - -### Changing Screens - -Long-pressing (>=650ms) Button 2 on the EuroPi will transition to the next display in the chain. If you transition past the last item in the display chain, then the display will cycle to the first item. - -The order of the displays is: -- Main display -- Clock Generator configuration - -#### Main Display - -The main display shows the voltages of the CV outputs on the EuroPi as well as the enabled status of the Clock Generator. - -While Clock Generator is operating, you can toggle its activation mode (default mode at startup is `on`) by pressing Button 1 on the EuroPi while on the main screen. When the clock is active, you will be informed by seeing a small bar ( `_` ) in the upper-left corner of the display. - -#### Clock Generator Configuration - -By default, the settings of Clock Generator are: -- BPM: 120.0 -- Gate Duration: 100.0 ms - - -When on the Clock Generator Configuration screen, pressing Button 1 on the EuroPi will cycle through the configuration items. The currently selected item for edit will be identified by an asterisk (`*`) character and it may be updated by turning Knob 1 of the EuroPi. Updates are applied immediately. - -## Special Thanks - -- Adam Wonak -- Charlotte Cox -- Allen Synthesis -- ALM -- Mouser Electronics -- Waveshare Electronics -- Raspberry Pi Foundation diff --git a/internal/projects/clockgenerator/clockgenerator.go b/internal/projects/clockgenerator/clockgenerator.go deleted file mode 100644 index d8b5a41..0000000 --- a/internal/projects/clockgenerator/clockgenerator.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/bootstrap" - "github.com/awonak/EuroPiGo/experimental/screenbank" - "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" - "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" -) - -type application struct { - clock *module.ClockGenerator - - ui *screenbank.ScreenBank - screenMain screen.Main - screenSettings screen.Settings -} - -func newApplication() (*application, error) { - clock := &module.ClockGenerator{} - app := &application{ - clock: clock, - - screenMain: screen.Main{ - Clock: clock, - }, - screenSettings: screen.Settings{ - Clock: clock, - }, - } - - var err error - app.ui, err = screenbank.NewScreenBank( - screenbank.WithScreen("main", "\u2b50", &app.screenMain), - screenbank.WithScreen("settings", "\u2611", &app.screenSettings), - ) - if err != nil { - return nil, err - } - - return app, nil -} - -func (app *application) Start(e *europi.EuroPi) { - if err := app.clock.Init(module.Config{ - BPM: 120.0, - GateDuration: time.Millisecond * 100, - Enabled: true, - ClockOut: func(value bool) { - if value { - e.CV1.SetCV(1.0) - } else { - e.CV1.SetCV(0.0) - } - bootstrap.ForceRepaintUI(e) - }, - }); err != nil { - panic(err) - } -} - -func (app *application) MainLoop(e *europi.EuroPi, deltaTime time.Duration) { - app.clock.Tick(deltaTime) -} - -func main() { - app, err := newApplication() - if err != nil { - panic(err) - } - - pi := europi.New() - - // some options shown below are being explicitly set to their defaults - // only to showcase their existence. - if err := bootstrap.Bootstrap( - pi, - bootstrap.EnableDisplayLogger(false), - bootstrap.InitRandom(true), - bootstrap.App( - app, - bootstrap.AppMainLoopInterval(time.Millisecond*1), - ), - bootstrap.UI( - app.ui, - bootstrap.UIRefreshRate(time.Millisecond*50), - ), - ); err != nil { - panic(err) - } -} diff --git a/internal/projects/clockgenerator/module/config.go b/internal/projects/clockgenerator/module/config.go deleted file mode 100644 index 3b5a2c0..0000000 --- a/internal/projects/clockgenerator/module/config.go +++ /dev/null @@ -1,14 +0,0 @@ -package module - -import "time" - -const ( - DefaultGateDuration = time.Millisecond * 100 -) - -type Config struct { - BPM float32 - GateDuration time.Duration - Enabled bool - ClockOut func(high bool) -} diff --git a/internal/projects/clockgenerator/module/module.go b/internal/projects/clockgenerator/module/module.go deleted file mode 100644 index b4935d3..0000000 --- a/internal/projects/clockgenerator/module/module.go +++ /dev/null @@ -1,119 +0,0 @@ -package module - -import ( - "fmt" - "time" - - "github.com/awonak/EuroPiGo/clamp" -) - -type ClockGenerator struct { - interval time.Duration - gateDuration time.Duration - enabled bool - clockOut func(high bool) - t time.Duration - gateT time.Duration - gateLevel bool - - bpm float32 // informational -} - -func (m *ClockGenerator) Init(config Config) error { - fnClockOut := config.ClockOut - if fnClockOut == nil { - fnClockOut = noopClockOut - } - m.clockOut = fnClockOut - - m.bpm = config.BPM - if config.BPM <= 0 { - return fmt.Errorf("invalid bpm setting: %v", config.BPM) - } - m.gateDuration = config.GateDuration - if m.gateDuration == 0 { - m.gateDuration = DefaultGateDuration - } - m.enabled = config.Enabled - - m.SetBPM(config.BPM) - return nil -} - -func noopClockOut(high bool) { -} - -func (m *ClockGenerator) Toggle() { - m.enabled = !m.enabled - m.t = 0 -} - -func (m *ClockGenerator) SetEnabled(enabled bool) { - m.enabled = enabled - m.t = 0 -} - -func (m *ClockGenerator) Enabled() bool { - return m.enabled -} - -func (m *ClockGenerator) SetBPM(bpm float32) { - if bpm == 0 { - bpm = 120.0 - } - m.bpm = bpm - m.interval = time.Duration(float32(time.Minute) / bpm) -} - -func (m *ClockGenerator) BPM() float32 { - return m.bpm -} - -func (m *ClockGenerator) SetGateDuration(dur time.Duration) { - if dur == 0 { - dur = DefaultGateDuration - } - - m.gateDuration = clamp.Clamp(dur, time.Microsecond, m.interval-time.Microsecond) -} - -func (m *ClockGenerator) GateDuration() time.Duration { - return m.gateDuration -} - -func (m *ClockGenerator) Tick(deltaTime time.Duration) { - if !m.enabled { - return - } - - prevGateLevel := m.gateLevel - - var reset bool - deltaTime, reset = m.processClockInterval(deltaTime) - - if reset { - m.gateT = 0 - m.gateLevel = true - } - - gateT := m.gateT + deltaTime - m.gateT = gateT % m.gateDuration - if gateT >= m.gateDuration { - m.gateLevel = false - } - - if m.gateLevel != prevGateLevel { - m.clockOut(m.gateLevel) - } -} - -func (m *ClockGenerator) processClockInterval(deltaTime time.Duration) (time.Duration, bool) { - t := m.t + deltaTime - m.t = t % m.interval - - if t >= m.interval { - return m.t, true - } - - return deltaTime, false -} diff --git a/internal/projects/clockgenerator/module/setting_bpm.go b/internal/projects/clockgenerator/module/setting_bpm.go deleted file mode 100644 index 6ad146c..0000000 --- a/internal/projects/clockgenerator/module/setting_bpm.go +++ /dev/null @@ -1,29 +0,0 @@ -package module - -import ( - "fmt" - - "github.com/awonak/EuroPiGo/lerp" - "github.com/awonak/EuroPiGo/units" -) - -const ( - MinBPM float32 = 0.1 - MaxBPM float32 = 480.0 -) - -var ( - bpmLerp = lerp.NewLerp32(MinBPM, MaxBPM) -) - -func BPMString(bpm float32) string { - return fmt.Sprintf(`%3.1f`, bpm) -} - -func BPMToCV(bpm float32) units.CV { - return units.CV(bpmLerp.ClampedInverseLerp(bpm)) -} - -func CVToBPM(cv units.CV) float32 { - return bpmLerp.ClampedLerpRound(cv.ToFloat32()) -} diff --git a/internal/projects/clockgenerator/module/setting_gateduration.go b/internal/projects/clockgenerator/module/setting_gateduration.go deleted file mode 100644 index 79f90bb..0000000 --- a/internal/projects/clockgenerator/module/setting_gateduration.go +++ /dev/null @@ -1,29 +0,0 @@ -package module - -import ( - "time" - - "github.com/awonak/EuroPiGo/lerp" - "github.com/awonak/EuroPiGo/units" -) - -const ( - MinGateDuration time.Duration = time.Microsecond - MaxGateDuration time.Duration = time.Millisecond * 990 -) - -var ( - gateDurationLerp = lerp.NewLerp32(MinGateDuration, MaxGateDuration) -) - -func GateDurationString(dur time.Duration) string { - return units.DurationString(dur) -} - -func GateDurationToCV(dur time.Duration) units.CV { - return units.CV(gateDurationLerp.ClampedInverseLerp(dur)) -} - -func CVToGateDuration(cv units.CV) time.Duration { - return gateDurationLerp.ClampedLerpRound(cv.ToFloat32()) -} diff --git a/internal/projects/clockgenerator/screen/main.go b/internal/projects/clockgenerator/screen/main.go deleted file mode 100644 index faa5cf2..0000000 --- a/internal/projects/clockgenerator/screen/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package screen - -import ( - "fmt" - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/experimental/draw" - "github.com/awonak/EuroPiGo/experimental/fontwriter" - "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" - "tinygo.org/x/tinydraw" - "tinygo.org/x/tinyfont/proggy" -) - -type Main struct { - Clock *module.ClockGenerator - writer fontwriter.Writer -} - -const ( - line1y int16 = 11 - line2y int16 = 23 -) - -var ( - DefaultFont = &proggy.TinySZ8pt7b -) - -func (m *Main) Start(e *europi.EuroPi) { - m.writer = fontwriter.Writer{ - Display: e.OLED, - Font: DefaultFont, - } -} - -func (m *Main) Button1Debounce() time.Duration { - return time.Millisecond * 200 -} - -func (m *Main) Button1(e *europi.EuroPi, deltaTime time.Duration) { - m.Clock.Toggle() -} - -func (m *Main) Paint(e *europi.EuroPi, deltaTime time.Duration) { - if m.Clock.Enabled() { - tinydraw.Line(e.OLED, 0, 0, 7, 0, draw.White) - } - m.writer.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y, draw.White) - m.writer.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y, draw.White) -} diff --git a/internal/projects/clockgenerator/screen/settings.go b/internal/projects/clockgenerator/screen/settings.go deleted file mode 100644 index 2722ef2..0000000 --- a/internal/projects/clockgenerator/screen/settings.go +++ /dev/null @@ -1,63 +0,0 @@ -package screen - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/experimental/knobmenu" - "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" - "github.com/awonak/EuroPiGo/units" -) - -type Settings struct { - km *knobmenu.KnobMenu - Clock *module.ClockGenerator -} - -func (m *Settings) bpmString() string { - return module.BPMString(m.Clock.BPM()) -} - -func (m *Settings) bpmValue() units.CV { - return module.BPMToCV(m.Clock.BPM()) -} - -func (m *Settings) setBPMValue(value units.CV) { - m.Clock.SetBPM(module.CVToBPM(value)) -} - -func (m *Settings) gateDurationString() string { - return module.GateDurationString(m.Clock.GateDuration()) -} - -func (m *Settings) gateDurationValue() units.CV { - return module.GateDurationToCV(m.Clock.GateDuration()) -} - -func (m *Settings) setGateDurationValue(value units.CV) { - m.Clock.SetGateDuration(module.CVToGateDuration(value)) -} - -func (m *Settings) Start(e *europi.EuroPi) { - km, err := knobmenu.NewKnobMenu(e.K1, - knobmenu.WithItem("bpm", "BPM", m.bpmString, m.bpmValue, m.setBPMValue), - knobmenu.WithItem("gateDuration", "Gate", m.gateDurationString, m.gateDurationValue, m.setGateDurationValue), - ) - if err != nil { - panic(err) - } - - m.km = km -} - -func (m *Settings) Button1Debounce() time.Duration { - return time.Millisecond * 200 -} - -func (m *Settings) Button1(e *europi.EuroPi, deltaTime time.Duration) { - m.km.Next() -} - -func (m *Settings) Paint(e *europi.EuroPi, deltaTime time.Duration) { - m.km.Paint(e, deltaTime) -} diff --git a/internal/projects/clockwerk/clockwerk.go b/internal/projects/clockwerk/clockwerk.go index e33ce0a..3d151d1 100644 --- a/internal/projects/clockwerk/clockwerk.go +++ b/internal/projects/clockwerk/clockwerk.go @@ -35,7 +35,6 @@ import ( "tinygo.org/x/tinyfont/proggy" europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/bootstrap" "github.com/awonak/EuroPiGo/clamp" "github.com/awonak/EuroPiGo/experimental/draw" "github.com/awonak/EuroPiGo/experimental/fontwriter" @@ -295,7 +294,7 @@ func main() { } // since we're not using a full bootstrap, manually activate the webservice (this is a no-op on pico) - if ws := bootstrap.ActivateNonPicoWS(e.Context(), e); ws != nil { + if ws := europi.ActivateNonPicoWS(e.Context(), e); ws != nil { defer func() { _ = ws.Shutdown() }() diff --git a/internal/projects/diagnostics/diagnostics.go b/internal/projects/diagnostics/diagnostics.go index f19167a..0bdf517 100644 --- a/internal/projects/diagnostics/diagnostics.go +++ b/internal/projects/diagnostics/diagnostics.go @@ -9,7 +9,6 @@ import ( "tinygo.org/x/tinyfont/proggy" europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/bootstrap" "github.com/awonak/EuroPiGo/experimental/draw" "github.com/awonak/EuroPiGo/experimental/fontwriter" ) @@ -95,7 +94,7 @@ func main() { } // since we're not using a full bootstrap, manually activate the webservice (this is a no-op on pico) - if ws := bootstrap.ActivateNonPicoWS(e.Context(), e); ws != nil { + if ws := europi.ActivateNonPicoWS(e.Context(), e); ws != nil { defer func() { _ = ws.Shutdown() }() diff --git a/internal/projects/randomskips/LICENSE.md b/internal/projects/randomskips/LICENSE.md deleted file mode 100644 index b7aa0e2..0000000 --- a/internal/projects/randomskips/LICENSE.md +++ /dev/null @@ -1,11 +0,0 @@ -# Released under MIT License - -Copyright (c) 2023 Jason Crawford - -Portions Copyright (c) 2022 Adam Wonak - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/internal/projects/randomskips/README.md b/internal/projects/randomskips/README.md deleted file mode 100644 index 6407b84..0000000 --- a/internal/projects/randomskips/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Random Skips - -A random gate skipper based on YouTube video observations made about the operation of the Ladik S-090 module. - -## Scope of This App - -The scope of this app is to drive the CV 1 output as a gate output based on a percentage chance of 33%. When the input gate (or internal clock gate) goes high (CV >= 0.8V), then a random value is generated and compared against the chance that's provided - if the probability is sufficient enough, then the gate is let through for as long as it is still high on the input. The moment the gate goes low, the output also goes low and the detection process starts again. - -### Inputs - -- Digital Input = clock input (optional, see below) - -### Outputs - -- CV 1 = Random Gate output - -## Using Random Skips - -### Changing Screens - -Long-pressing (>=650ms) Button 2 on the EuroPi will transition to the next display in the chain. If you transition past the last item in the display chain, then the display will cycle to the first item. - -The order of the displays is: -- Main display -- Random Skips configuration -- Performance clock configuration - -#### Main Display - -The main display shows the voltages of the CV outputs on the EuroPi as well as the enabled status of the internal performance clock. - -While Random Skips is operating, you can toggle between using the external clock (default mode at startup) and the internal clock by pressing Button 1 on the EuroPi while on the main screen. When the internal clock mode is active, you will be informed by seeing a small bar ( `_` ) in the upper-left corner of the display. - -#### Random Skips Configuration - -By default, the settings of Random Skips are: -- Chance: 50.0% - -When on the Random Skips Configuration screen, pressing Button 1 on the EuroPi will cycle through the configuration items. The currently selected item for edit will be identified by an asterisk (`*`) character and it may be updated by turning Knob 1 of the EuroPi. Updates are applied immediately. - -#### Performance Clock Configuration - -By default, the settings of the Performance Clock are: -- Clock Rate: 120.0 BPM -- Gate Duration: 100.0 ms - -When on the Performance Clock Configuration screen, pressing Button 1 on the EuroPi will cycle through the configuration items. The currently selected item for edit will be identified by an asterisk (`*`) character and it may be updated by turning Knob 1 of the EuroPi. Updates are applied immediately. - -## Special Thanks - -- Adam Wonak -- Charlotte Cox -- Allen Synthesis -- Ladik.eu -- Mouser Electronics -- Waveshare Electronics -- Raspberry Pi Foundation diff --git a/internal/projects/randomskips/module/config.go b/internal/projects/randomskips/module/config.go deleted file mode 100644 index d862339..0000000 --- a/internal/projects/randomskips/module/config.go +++ /dev/null @@ -1,6 +0,0 @@ -package module - -type Config struct { - Gate func(high bool) - Chance float32 -} diff --git a/internal/projects/randomskips/module/module.go b/internal/projects/randomskips/module/module.go deleted file mode 100644 index b4fc2ca..0000000 --- a/internal/projects/randomskips/module/module.go +++ /dev/null @@ -1,65 +0,0 @@ -package module - -import ( - "math/rand" - "time" - - "github.com/awonak/EuroPiGo/units" -) - -type RandomSkips struct { - gate func(high bool) - chance float32 - - active bool - lastInput bool - cv float32 - ac float32 // attenuated chance (cv * chance) -} - -func (m *RandomSkips) Init(config Config) error { - fnGate := config.Gate - if fnGate == nil { - fnGate = noopGate - } - m.gate = fnGate - m.chance = config.Chance - - m.SetCV(1) - return nil -} - -func noopGate(high bool) { -} - -func (m *RandomSkips) Gate(value bool) { - prev := m.active - lastInput := m.lastInput - next := prev - m.lastInput = value - - if value != lastInput && rand.Float32() < m.ac { - next = !prev - } - - if prev != next { - m.active = next - m.gate(next) - } -} - -func (m *RandomSkips) SetChance(chance float32) { - m.chance = chance -} - -func (m *RandomSkips) Chance() float32 { - return m.chance -} - -func (m *RandomSkips) SetCV(cv units.CV) { - m.cv = cv.ToFloat32() - m.ac = m.chance * m.cv -} - -func (m *RandomSkips) Tick(deltaTime time.Duration) { -} diff --git a/internal/projects/randomskips/module/setting_chance.go b/internal/projects/randomskips/module/setting_chance.go deleted file mode 100644 index 74a95fc..0000000 --- a/internal/projects/randomskips/module/setting_chance.go +++ /dev/null @@ -1,20 +0,0 @@ -package module - -import ( - "fmt" - - "github.com/awonak/EuroPiGo/clamp" - "github.com/awonak/EuroPiGo/units" -) - -func ChanceString(chance float32) string { - return fmt.Sprintf("%3.1f%%", chance*100.0) -} - -func ChanceToCV(chance float32) units.CV { - return units.CV(chance) -} - -func CVToChance(cv units.CV) float32 { - return clamp.Clamp(cv.ToFloat32(), 0.0, 1.0) -} diff --git a/internal/projects/randomskips/randomskips.go b/internal/projects/randomskips/randomskips.go deleted file mode 100644 index dc2cc50..0000000 --- a/internal/projects/randomskips/randomskips.go +++ /dev/null @@ -1,119 +0,0 @@ -package main - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/bootstrap" - "github.com/awonak/EuroPiGo/experimental/screenbank" - "github.com/awonak/EuroPiGo/hardware/hal" - clockgenerator "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" - clockScreen "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/screen" - "github.com/awonak/EuroPiGo/internal/projects/randomskips/module" - "github.com/awonak/EuroPiGo/internal/projects/randomskips/screen" -) - -func makeGate(out hal.VoltageOutput) func(value bool) { - return func(value bool) { - if value { - out.SetCV(1.0) - } else { - out.SetCV(0.0) - } - } -} - -type application struct { - skip *module.RandomSkips - clock *clockgenerator.ClockGenerator - - ui *screenbank.ScreenBank - screenMain screen.Main - screenClock clockScreen.Settings - screenSettings screen.Settings -} - -func newApplication() (*application, error) { - skip := &module.RandomSkips{} - clock := &clockgenerator.ClockGenerator{} - - app := &application{ - skip: skip, - clock: clock, - screenMain: screen.Main{ - RandomSkips: skip, - Clock: clock, - }, - screenClock: clockScreen.Settings{ - Clock: clock, - }, - screenSettings: screen.Settings{ - RandomSkips: skip, - }, - } - - var err error - app.ui, err = screenbank.NewScreenBank( - screenbank.WithScreen("main", "\u2b50", &app.screenMain), - screenbank.WithScreen("settings", "\u2611", &app.screenSettings), - screenbank.WithScreen("clock", "\u23f0", &app.screenClock), - ) - if err != nil { - return nil, err - } - - return app, nil -} - -func (app *application) Start(e *europi.EuroPi) { - if err := app.skip.Init(module.Config{ - Gate: makeGate(e.CV1), - Chance: 0.5, - }); err != nil { - panic(err) - } - - if err := app.clock.Init(clockgenerator.Config{ - BPM: 120.0, - Enabled: false, - ClockOut: app.skip.Gate, - }); err != nil { - panic(err) - } - - e.DI.HandlerEx(hal.ChangeAny, func(value bool, _ time.Duration) { - app.skip.Gate(value) - }) -} - -func (app *application) MainLoop(e *europi.EuroPi, deltaTime time.Duration) { - app.clock.Tick(deltaTime) - app.skip.Tick(deltaTime) -} - -func main() { - app, err := newApplication() - if err != nil { - panic(err) - } - - pi := europi.New() - - // some options shown below are being explicitly set to their defaults - // only to showcase their existence. - if err := bootstrap.Bootstrap( - pi, - bootstrap.EnableDisplayLogger(false), - bootstrap.InitRandom(true), - bootstrap.App( - app, - bootstrap.AppMainLoopInterval(time.Millisecond*1), - ), - bootstrap.UI( - app.ui, - bootstrap.UIRefreshRate(time.Millisecond*50), - ), - ); err != nil { - panic(err) - } -} diff --git a/internal/projects/randomskips/screen/main.go b/internal/projects/randomskips/screen/main.go deleted file mode 100644 index 128f6cd..0000000 --- a/internal/projects/randomskips/screen/main.go +++ /dev/null @@ -1,52 +0,0 @@ -package screen - -import ( - "fmt" - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/experimental/draw" - "github.com/awonak/EuroPiGo/experimental/fontwriter" - clockgenerator "github.com/awonak/EuroPiGo/internal/projects/clockgenerator/module" - "github.com/awonak/EuroPiGo/internal/projects/randomskips/module" - "tinygo.org/x/tinydraw" - "tinygo.org/x/tinyfont/proggy" -) - -type Main struct { - RandomSkips *module.RandomSkips - Clock *clockgenerator.ClockGenerator - writer fontwriter.Writer -} - -const ( - line1y int16 = 11 - line2y int16 = 23 -) - -var ( - DefaultFont = &proggy.TinySZ8pt7b -) - -func (m *Main) Start(e *europi.EuroPi) { - m.writer = fontwriter.Writer{ - Display: e.OLED, - Font: DefaultFont, - } -} - -func (m *Main) Button1Debounce() time.Duration { - return time.Millisecond * 200 -} - -func (m *Main) Button1(e *europi.EuroPi, deltaTime time.Duration) { - m.Clock.Toggle() -} - -func (m *Main) Paint(e *europi.EuroPi, deltaTime time.Duration) { - if m.Clock.Enabled() { - tinydraw.Line(m.writer.Display, 0, 0, 7, 0, draw.White) - } - m.writer.WriteLine(fmt.Sprintf("1:%2.1f 2:%2.1f 3:%2.1f", e.CV1.Voltage(), e.CV2.Voltage(), e.CV3.Voltage()), 0, line1y, draw.White) - m.writer.WriteLine(fmt.Sprintf("4:%2.1f 5:%2.1f 6:%2.1f", e.CV4.Voltage(), e.CV5.Voltage(), e.CV6.Voltage()), 0, line2y, draw.White) -} diff --git a/internal/projects/randomskips/screen/settings.go b/internal/projects/randomskips/screen/settings.go deleted file mode 100644 index 0884b24..0000000 --- a/internal/projects/randomskips/screen/settings.go +++ /dev/null @@ -1,50 +0,0 @@ -package screen - -import ( - "time" - - europi "github.com/awonak/EuroPiGo" - "github.com/awonak/EuroPiGo/experimental/knobmenu" - "github.com/awonak/EuroPiGo/internal/projects/randomskips/module" - "github.com/awonak/EuroPiGo/units" -) - -type Settings struct { - km *knobmenu.KnobMenu - RandomSkips *module.RandomSkips -} - -func (m *Settings) chanceString() string { - return module.ChanceString(m.RandomSkips.Chance()) -} - -func (m *Settings) chanceValue() units.CV { - return module.ChanceToCV(m.RandomSkips.Chance()) -} - -func (m *Settings) setChanceValue(value units.CV) { - m.RandomSkips.SetChance(module.CVToChance(value)) -} - -func (m *Settings) Start(e *europi.EuroPi) { - km, err := knobmenu.NewKnobMenu(e.K1, - knobmenu.WithItem("chance", "Chance", m.chanceString, m.chanceValue, m.setChanceValue), - ) - if err != nil { - panic(err) - } - - m.km = km -} - -func (m *Settings) Button1Debounce() time.Duration { - return time.Millisecond * 200 -} - -func (m *Settings) Button1(e *europi.EuroPi, deltaTime time.Duration) { - m.km.Next() -} - -func (m *Settings) Paint(e *europi.EuroPi, deltaTime time.Duration) { - m.km.Paint(e, deltaTime) -} From e6258de422a87d84039096b3da20cfc4a6b9be24 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 6 May 2023 13:25:58 -0700 Subject: [PATCH 61/62] missed in last checkin --- debug_nonpico.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debug_nonpico.go b/debug_nonpico.go index dc3bf0b..d8baccf 100644 --- a/debug_nonpico.go +++ b/debug_nonpico.go @@ -1,7 +1,7 @@ //go:build !pico // +build !pico -package bootstrap +package europi import ( "context" From 543ef6214725e51f4ddf1ccdc7765ef664bc3a57 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 6 May 2023 16:03:33 -0700 Subject: [PATCH 62/62] Fix for tinygo getting confused about generics - simplify setup while allowing for more complex calibration --- experimental/envelope/map.go | 17 ---- experimental/envelope/map32.go | 150 ------------------------------ experimental/envelope/map64.go | 150 ------------------------------ experimental/envelope/mapentry.go | 22 ----- hardware/common/analoginput.go | 4 +- hardware/hal/analoginput.go | 4 +- hardware/hal/voltageoutput.go | 4 +- hardware/rev0/analoginput.go | 15 +-- hardware/rev0/voltageoutput.go | 15 +-- hardware/rev1/analoginput.go | 15 +-- hardware/rev1/voltageoutput.go | 15 +-- internal/nonpico/common/pwm.go | 6 +- internal/nonpico/rev0/platform.go | 19 ++-- internal/nonpico/rev1/platform.go | 14 ++- internal/pico/pwm.go | 4 +- lerp/lerp.go | 2 + lerp/lerp32.go | 12 ++- lerp/lerp32_test.go | 16 ++++ lerp/lerp64.go | 12 ++- lerp/lerp64_test.go | 16 ++++ lerp/remap.go | 16 +--- lerp/remap32.go | 57 +++--------- lerp/remap32_test.go | 22 +---- lerp/remap64.go | 57 +++--------- lerp/remap64_test.go | 22 ----- lerp/remappoint.go | 53 ----------- lerp/remappoint_test.go | 129 ------------------------- 27 files changed, 126 insertions(+), 742 deletions(-) delete mode 100644 experimental/envelope/map.go delete mode 100644 experimental/envelope/map32.go delete mode 100644 experimental/envelope/map64.go delete mode 100644 experimental/envelope/mapentry.go delete mode 100644 lerp/remappoint.go delete mode 100644 lerp/remappoint_test.go diff --git a/experimental/envelope/map.go b/experimental/envelope/map.go deleted file mode 100644 index 2e9fa7f..0000000 --- a/experimental/envelope/map.go +++ /dev/null @@ -1,17 +0,0 @@ -package envelope - -import "github.com/awonak/EuroPiGo/lerp" - -type Map[TIn, TOut lerp.Lerpable] interface { - Remap(value TIn) TOut - Unmap(value TOut) TIn - InputMinimum() TIn - InputMaximum() TIn - OutputMinimum() TOut - OutputMaximum() TOut -} - -type remapList[TIn, TOut lerp.Lerpable, TFloat lerp.Float] struct { - lerp.Remapper[TIn, TOut, TFloat] - nextOut *remapList[TIn, TOut, TFloat] -} diff --git a/experimental/envelope/map32.go b/experimental/envelope/map32.go deleted file mode 100644 index 7fc605e..0000000 --- a/experimental/envelope/map32.go +++ /dev/null @@ -1,150 +0,0 @@ -package envelope - -import ( - "sort" - - "github.com/awonak/EuroPiGo/lerp" -) - -type envMap32[TIn, TOut lerp.Lerpable] struct { - rem []remapList[TIn, TOut, float32] - outMax TOut - outRoot *remapList[TIn, TOut, float32] -} - -func NewLerpMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { - if len(points) == 0 { - panic("must have at least 1 point") - } - - p := make(MapEntryList[TIn, TOut], len(points)) - // make a copy just in case we're dealing with another goroutine's data - copy(p, points) - // ensure it's sorted - sort.Sort(p) - - var rem []remapList[TIn, TOut, float32] - if len(p) > 1 { - for pos := 0; pos < len(p)-1; pos++ { - cur, next := p[pos], p[pos+1] - rem = append(rem, remapList[TIn, TOut, float32]{ - Remapper: lerp.NewRemap32(cur.Input, next.Input, cur.Output, next.Output), - }) - } - } - last := &p[len(p)-1] - rem = append(rem, remapList[TIn, TOut, float32]{ - Remapper: lerp.NewRemapPoint[TIn, TOut, float32](last.Input, last.Output), - }) - - outSort := make(MapEntryList[TOut, int], len(rem)) - for i, e := range rem { - outSort[i].Input = e.OutputMinimum() - outSort[i].Output = i - } - sort.Sort(outSort) - rootIdx := outSort[0].Output - outRoot := &rem[rootIdx] - for pos := 0; pos < len(rem)-1; pos++ { - cur, next := outSort[pos].Output, outSort[pos+1].Output - rem[cur].nextOut = &rem[next] - } - - return &envMap32[TIn, TOut]{ - rem: rem, - outMax: last.Output, - outRoot: outRoot, - } -} - -func NewPointMap32[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { - if len(points) == 0 { - panic("must have at least 1 point") - } - - p := make(MapEntryList[TIn, TOut], len(points)) - // make a copy just in case we're dealing with another goroutine's data - copy(p, points) - // ensure it's sorted - sort.Sort(p) - - var rem []remapList[TIn, TOut, float32] - for _, cur := range p { - rem = append(rem, remapList[TIn, TOut, float32]{ - Remapper: lerp.NewRemapPoint[TIn, TOut, float32](cur.Input, cur.Output), - }) - } - last := &p[len(p)-1] - - outSort := make(MapEntryList[TOut, int], len(rem)) - for i, e := range rem { - outSort[i].Input = e.OutputMinimum() - outSort[i].Output = i - } - sort.Sort(outSort) - rootIdx := outSort[0].Output - outRoot := &rem[rootIdx] - for pos := 0; pos < len(rem)-1; pos++ { - cur, next := outSort[pos].Output, outSort[pos+1].Output - rem[cur].nextOut = &rem[next] - } - - return &envMap32[TIn, TOut]{ - rem: rem, - outMax: last.Output, - outRoot: outRoot, - } -} - -func (m *envMap32[TIn, TOut]) Remap(value TIn) TOut { - for _, r := range m.rem { - if value < r.InputMinimum() { - return r.OutputMinimum() - } else if value < r.InputMaximum() { - return r.Remap(value) - } - } - - return m.outMax -} - -func (m *envMap32[TIn, TOut]) Unmap(value TOut) TIn { - for r := m.outRoot; r != nil; r = r.nextOut { - outMin := r.OutputMinimum() - outMax := r.OutputMaximum() - if outMin < outMax { - if value < outMin { - return r.InputMinimum() - } else if value <= outMax { - return r.Unmap(value) - } - } else { - if value < outMax { - return r.InputMinimum() - } else if value <= outMin { - return r.Unmap(value) - } - } - } - - return m.InputMaximum() -} - -func (m *envMap32[TIn, TOut]) InputMinimum() TIn { - // we're guaranteed to have 1 point - return m.rem[0].InputMaximum() -} - -func (m *envMap32[TIn, TOut]) InputMaximum() TIn { - // we're guaranteed to have 1 point - return m.rem[len(m.rem)-1].InputMaximum() -} - -func (m *envMap32[TIn, TOut]) OutputMinimum() TOut { - // we're guaranteed to have 1 point - return m.rem[0].OutputMinimum() -} - -func (m *envMap32[TIn, TOut]) OutputMaximum() TOut { - return m.outMax -} diff --git a/experimental/envelope/map64.go b/experimental/envelope/map64.go deleted file mode 100644 index 121b95f..0000000 --- a/experimental/envelope/map64.go +++ /dev/null @@ -1,150 +0,0 @@ -package envelope - -import ( - "sort" - - "github.com/awonak/EuroPiGo/lerp" -) - -type envMap64[TIn, TOut lerp.Lerpable] struct { - rem []remapList[TIn, TOut, float64] - outMax TOut - outRoot *remapList[TIn, TOut, float64] -} - -func NewLerpMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { - if len(points) == 0 { - panic("must have at least 1 point") - } - - p := make(MapEntryList[TIn, TOut], len(points)) - // make a copy just in case we're dealing with another goroutine's data - copy(p, points) - // ensure it's sorted - sort.Sort(p) - - var rem []remapList[TIn, TOut, float64] - if len(p) > 1 { - for pos := 0; pos < len(p)-1; pos++ { - cur, next := p[pos], p[pos+1] - rem = append(rem, remapList[TIn, TOut, float64]{ - Remapper: lerp.NewRemap64(cur.Input, next.Input, cur.Output, next.Output), - }) - } - } - last := &p[len(p)-1] - rem = append(rem, remapList[TIn, TOut, float64]{ - Remapper: lerp.NewRemapPoint[TIn, TOut, float64](last.Input, last.Output), - }) - - outSort := make(MapEntryList[TOut, int], len(rem)) - for i, e := range rem { - outSort[i].Input = e.OutputMinimum() - outSort[i].Output = i - } - sort.Sort(outSort) - rootIdx := outSort[0].Output - outRoot := &rem[rootIdx] - for pos := 0; pos < len(rem)-1; pos++ { - cur, next := outSort[pos].Output, outSort[pos+1].Output - rem[cur].nextOut = &rem[next] - } - - return &envMap64[TIn, TOut]{ - rem: rem, - outMax: last.Output, - outRoot: outRoot, - } -} - -func NewPointMap64[TIn, TOut lerp.Lerpable](points []MapEntry[TIn, TOut]) Map[TIn, TOut] { - if len(points) == 0 { - panic("must have at least 1 point") - } - - p := make(MapEntryList[TIn, TOut], len(points)) - // make a copy just in case we're dealing with another goroutine's data - copy(p, points) - // ensure it's sorted - sort.Sort(p) - - var rem []remapList[TIn, TOut, float64] - for _, cur := range p { - rem = append(rem, remapList[TIn, TOut, float64]{ - Remapper: lerp.NewRemapPoint[TIn, TOut, float64](cur.Input, cur.Output), - }) - } - last := &p[len(p)-1] - - outSort := make(MapEntryList[TOut, int], len(rem)) - for i, e := range rem { - outSort[i].Input = e.OutputMinimum() - outSort[i].Output = i - } - sort.Sort(outSort) - rootIdx := outSort[0].Output - outRoot := &rem[rootIdx] - for pos := 0; pos < len(rem)-1; pos++ { - cur, next := outSort[pos].Output, outSort[pos+1].Output - rem[cur].nextOut = &rem[next] - } - - return &envMap64[TIn, TOut]{ - rem: rem, - outMax: last.Output, - outRoot: outRoot, - } -} - -func (m *envMap64[TIn, TOut]) Remap(value TIn) TOut { - for _, r := range m.rem { - if value < r.InputMinimum() { - return r.OutputMinimum() - } else if value < r.InputMaximum() { - return r.Remap(value) - } - } - - return m.outMax -} - -func (m *envMap64[TIn, TOut]) Unmap(value TOut) TIn { - for r := m.outRoot; r != nil; r = r.nextOut { - outMin := r.OutputMinimum() - outMax := r.OutputMaximum() - if outMin < outMax { - if value < outMin { - return r.InputMinimum() - } else if value <= outMax { - return r.Unmap(value) - } - } else { - if value < outMax { - return r.InputMinimum() - } else if value <= outMin { - return r.Unmap(value) - } - } - } - - return m.InputMaximum() -} - -func (m *envMap64[TIn, TOut]) InputMinimum() TIn { - // we're guaranteed to have 1 point - return m.rem[0].InputMaximum() -} - -func (m *envMap64[TIn, TOut]) InputMaximum() TIn { - // we're guaranteed to have 1 point - return m.rem[len(m.rem)-1].InputMaximum() -} - -func (m *envMap64[TIn, TOut]) OutputMinimum() TOut { - // we're guaranteed to have 1 point - return m.rem[0].OutputMinimum() -} - -func (m *envMap64[TIn, TOut]) OutputMaximum() TOut { - return m.outMax -} diff --git a/experimental/envelope/mapentry.go b/experimental/envelope/mapentry.go deleted file mode 100644 index 6c74f88..0000000 --- a/experimental/envelope/mapentry.go +++ /dev/null @@ -1,22 +0,0 @@ -package envelope - -import "github.com/awonak/EuroPiGo/lerp" - -type MapEntry[TIn, TOut lerp.Lerpable] struct { - Input TIn - Output TOut -} - -type MapEntryList[TIn, TOut lerp.Lerpable] []MapEntry[TIn, TOut] - -func (m MapEntryList[TIn, TOut]) Len() int { - return len(m) -} - -func (m MapEntryList[TIn, TOut]) Less(i, j int) bool { - return m[i].Input < m[j].Input -} - -func (m MapEntryList[TIn, TOut]) Swap(i, j int) { - m[i], m[j] = m[j], m[i] -} diff --git a/hardware/common/analoginput.go b/hardware/common/analoginput.go index 4f0a313..0ceb501 100644 --- a/hardware/common/analoginput.go +++ b/hardware/common/analoginput.go @@ -4,8 +4,8 @@ import ( "errors" "github.com/awonak/EuroPiGo/clamp" - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/lerp" "github.com/awonak/EuroPiGo/units" ) @@ -14,7 +14,7 @@ import ( type Analoginput struct { adc ADCProvider samples int - cal envelope.Map[uint16, float32] + cal lerp.Remapper32[uint16, float32] } var ( diff --git a/hardware/hal/analoginput.go b/hardware/hal/analoginput.go index 010d01c..26b3fcf 100644 --- a/hardware/hal/analoginput.go +++ b/hardware/hal/analoginput.go @@ -1,7 +1,7 @@ package hal import ( - "github.com/awonak/EuroPiGo/experimental/envelope" + "github.com/awonak/EuroPiGo/lerp" "github.com/awonak/EuroPiGo/units" ) @@ -19,5 +19,5 @@ type AnalogInput interface { type AnalogInputConfig struct { Samples int - Calibration envelope.Map[uint16, float32] + Calibration lerp.Remapper32[uint16, float32] } diff --git a/hardware/hal/voltageoutput.go b/hardware/hal/voltageoutput.go index 62b1c44..834ba1d 100644 --- a/hardware/hal/voltageoutput.go +++ b/hardware/hal/voltageoutput.go @@ -3,7 +3,7 @@ package hal import ( "time" - "github.com/awonak/EuroPiGo/experimental/envelope" + "github.com/awonak/EuroPiGo/lerp" "github.com/awonak/EuroPiGo/units" ) @@ -20,5 +20,5 @@ type VoltageOutput interface { type VoltageOutputConfig struct { Period time.Duration Monopolar bool - Calibration envelope.Map[float32, uint16] + Calibration lerp.Remapper32[float32, uint16] } diff --git a/hardware/rev0/analoginput.go b/hardware/rev0/analoginput.go index 494f1e9..e0eb273 100644 --- a/hardware/rev0/analoginput.go +++ b/hardware/rev0/analoginput.go @@ -1,8 +1,8 @@ package rev0 import ( - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/lerp" ) const ( @@ -18,19 +18,10 @@ const ( ) var ( - AnalogInputCalibrationPoints = []envelope.MapEntry[uint16, float32]{ - { - Input: DefaultCalibratedMinAI, - Output: MinInputVoltage, - }, - { - Input: DefaultCalibratedMaxAI, - Output: MaxInputVoltage, - }, - } + DefaultAICalibration = lerp.NewRemap32[uint16, float32](DefaultCalibratedMinAI, DefaultCalibratedMaxAI, MinInputVoltage, MaxInputVoltage) aiInitialConfig = hal.AnalogInputConfig{ Samples: DefaultSamples, - Calibration: envelope.NewLerpMap32(AnalogInputCalibrationPoints), + Calibration: DefaultAICalibration, } ) diff --git a/hardware/rev0/voltageoutput.go b/hardware/rev0/voltageoutput.go index 8aa0332..b3ff4c8 100644 --- a/hardware/rev0/voltageoutput.go +++ b/hardware/rev0/voltageoutput.go @@ -3,8 +3,8 @@ package rev0 import ( "time" - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/lerp" ) const ( @@ -23,20 +23,11 @@ const ( ) var ( - VoltageOutputCalibrationPoints = []envelope.MapEntry[float32, uint16]{ - { - Input: MinOutputVoltage, - Output: CalibratedTop, - }, - { - Input: MaxOutputVoltage, - Output: CalibratedOffset, - }, - } + DefaultVoltageOutputCalibration = lerp.NewRemap32[float32, uint16](MinOutputVoltage, MaxOutputVoltage, CalibratedOffset, CalibratedTop) cvInitialConfig = hal.VoltageOutputConfig{ Period: DefaultPWMPeriod, Monopolar: true, - Calibration: envelope.NewLerpMap32(VoltageOutputCalibrationPoints), + Calibration: DefaultVoltageOutputCalibration, } ) diff --git a/hardware/rev1/analoginput.go b/hardware/rev1/analoginput.go index 787b9ce..f78ccf0 100644 --- a/hardware/rev1/analoginput.go +++ b/hardware/rev1/analoginput.go @@ -1,8 +1,8 @@ package rev1 import ( - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/lerp" ) const ( @@ -18,19 +18,10 @@ const ( ) var ( - AnalogInputCalibrationPoints = []envelope.MapEntry[uint16, float32]{ - { - Input: DefaultCalibratedMinAI, - Output: MinInputVoltage, - }, - { - Input: DefaultCalibratedMaxAI, - Output: MaxInputVoltage, - }, - } + DefaultAICalibration = lerp.NewRemap32[uint16, float32](DefaultCalibratedMinAI, DefaultCalibratedMaxAI, MinInputVoltage, MaxInputVoltage) aiInitialConfig = hal.AnalogInputConfig{ Samples: DefaultSamples, - Calibration: envelope.NewLerpMap32(AnalogInputCalibrationPoints), + Calibration: DefaultAICalibration, } ) diff --git a/hardware/rev1/voltageoutput.go b/hardware/rev1/voltageoutput.go index 3da6c56..5b2c14f 100644 --- a/hardware/rev1/voltageoutput.go +++ b/hardware/rev1/voltageoutput.go @@ -3,8 +3,8 @@ package rev1 import ( "time" - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/lerp" ) const ( @@ -23,20 +23,11 @@ const ( ) var ( - VoltageOutputCalibrationPoints = []envelope.MapEntry[float32, uint16]{ - { - Input: MinOutputVoltage, - Output: CalibratedTop, - }, - { - Input: MaxOutputVoltage, - Output: CalibratedOffset, - }, - } + DefaultVoltageOutputCalibration = lerp.NewRemap32[float32, uint16](MinOutputVoltage, MaxOutputVoltage, CalibratedOffset, CalibratedTop) cvInitialConfig = hal.VoltageOutputConfig{ Period: DefaultPWMPeriod, Monopolar: true, - Calibration: envelope.NewLerpMap32(VoltageOutputCalibrationPoints), + Calibration: DefaultVoltageOutputCalibration, } ) diff --git a/internal/nonpico/common/pwm.go b/internal/nonpico/common/pwm.go index 8455d34..073b704 100644 --- a/internal/nonpico/common/pwm.go +++ b/internal/nonpico/common/pwm.go @@ -6,14 +6,14 @@ package common import ( "fmt" - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/common" "github.com/awonak/EuroPiGo/hardware/hal" + "github.com/awonak/EuroPiGo/lerp" ) type nonPicoPwm struct { id hal.HardwareId - cal envelope.Map[float32, uint16] + cal lerp.Remapper32[float32, uint16] v float32 } @@ -22,7 +22,7 @@ var ( _ common.PWMProvider = (*nonPicoPwm)(nil) ) -func NewNonPicoPwm(id hal.HardwareId, cal envelope.Map[float32, uint16]) *nonPicoPwm { +func NewNonPicoPwm(id hal.HardwareId, cal lerp.Remapper32[float32, uint16]) *nonPicoPwm { p := &nonPicoPwm{ id: id, cal: cal, diff --git a/internal/nonpico/rev0/platform.go b/internal/nonpico/rev0/platform.go index 27dcebe..128d060 100644 --- a/internal/nonpico/rev0/platform.go +++ b/internal/nonpico/rev0/platform.go @@ -1,27 +1,24 @@ package rev0 import ( - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/rev0" "github.com/awonak/EuroPiGo/internal/nonpico/common" ) func DoInit() { - ajCalMap := envelope.NewLerpMap32(rev0.VoltageOutputCalibrationPoints) - djCalMap := envelope.NewPointMap32(rev0.VoltageOutputCalibrationPoints) 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, ajCalMap), - OutputAnalog2: common.NewNonPicoPwm(rev0.HardwareIdAnalog2Output, ajCalMap), - OutputAnalog3: common.NewNonPicoPwm(rev0.HardwareIdAnalog3Output, ajCalMap), - OutputAnalog4: common.NewNonPicoPwm(rev0.HardwareIdAnalog4Output, ajCalMap), - OutputDigital1: common.NewNonPicoPwm(rev0.HardwareIdDigital1Output, djCalMap), - OutputDigital2: common.NewNonPicoPwm(rev0.HardwareIdDigital2Output, djCalMap), - OutputDigital3: common.NewNonPicoPwm(rev0.HardwareIdDigital3Output, djCalMap), - OutputDigital4: common.NewNonPicoPwm(rev0.HardwareIdDigital4Output, djCalMap), + 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/rev1/platform.go b/internal/nonpico/rev1/platform.go index bd1687b..169d266 100644 --- a/internal/nonpico/rev1/platform.go +++ b/internal/nonpico/rev1/platform.go @@ -1,13 +1,11 @@ package rev1 import ( - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/rev1" "github.com/awonak/EuroPiGo/internal/nonpico/common" ) func DoInit() { - cvCalMap := envelope.NewLerpMap32(rev1.VoltageOutputCalibrationPoints) rev1.Initialize(rev1.InitializationParameters{ InputDigital1: common.NewNonPicoDigitalReader(rev1.HardwareIdDigital1Input), InputAnalog1: common.NewNonPicoAdc(rev1.HardwareIdAnalog1Input), @@ -16,12 +14,12 @@ func DoInit() { InputButton2: common.NewNonPicoDigitalReader(rev1.HardwareIdButton2Input), InputKnob1: common.NewNonPicoAdc(rev1.HardwareIdKnob1Input), InputKnob2: common.NewNonPicoAdc(rev1.HardwareIdKnob2Input), - OutputVoltage1: common.NewNonPicoPwm(rev1.HardwareIdCV1Output, cvCalMap), - OutputVoltage2: common.NewNonPicoPwm(rev1.HardwareIdCV2Output, cvCalMap), - OutputVoltage3: common.NewNonPicoPwm(rev1.HardwareIdCV3Output, cvCalMap), - OutputVoltage4: common.NewNonPicoPwm(rev1.HardwareIdCV4Output, cvCalMap), - OutputVoltage5: common.NewNonPicoPwm(rev1.HardwareIdCV5Output, cvCalMap), - OutputVoltage6: common.NewNonPicoPwm(rev1.HardwareIdCV6Output, cvCalMap), + 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/pico/pwm.go b/internal/pico/pwm.go index e35ee96..ce5259f 100644 --- a/internal/pico/pwm.go +++ b/internal/pico/pwm.go @@ -11,9 +11,9 @@ import ( "sync/atomic" "time" - "github.com/awonak/EuroPiGo/experimental/envelope" "github.com/awonak/EuroPiGo/hardware/hal" "github.com/awonak/EuroPiGo/hardware/rev0" + "github.com/awonak/EuroPiGo/lerp" ) type picoPwm struct { @@ -23,7 +23,7 @@ type picoPwm struct { v uint32 period time.Duration monopolar bool - cal envelope.Map[float32, uint16] + cal lerp.Remapper32[float32, uint16] } // pwmGroup is an interface for interacting with a machine.pwmGroup diff --git a/lerp/lerp.go b/lerp/lerp.go index 902cef0..bd6e9d2 100644 --- a/lerp/lerp.go +++ b/lerp/lerp.go @@ -15,6 +15,8 @@ type Lerper[T Lerpable, F Float] interface { ClampedLerpRound(t F) T InverseLerp(v T) F ClampedInverseLerp(v T) F + OutputMinimum() T + OutputMaximum() T } type Lerper32[T Lerpable] Lerper[T, float32] diff --git a/lerp/lerp32.go b/lerp/lerp32.go index fbfe4db..ba1a9f1 100644 --- a/lerp/lerp32.go +++ b/lerp/lerp32.go @@ -32,14 +32,22 @@ func (l lerp32[T]) ClampedLerpRound(t float32) T { func (l lerp32[T]) InverseLerp(v T) float32 { if l.r != 0.0 { - return float32(v-l.b) / l.r + 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-l.b)/l.r, 0.0, 1.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 index 4b60c0e..e4e76e3 100644 --- a/lerp/lerp32_test.go +++ b/lerp/lerp32_test.go @@ -261,4 +261,20 @@ func TestLerp32(t *testing.T) { }) }) }) + + 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 index f3a6166..9c0ba9b 100644 --- a/lerp/lerp64.go +++ b/lerp/lerp64.go @@ -32,14 +32,22 @@ func (l lerp64[T]) ClampedLerpRound(t float64) T { func (l lerp64[T]) InverseLerp(v T) float64 { if l.r != 0.0 { - return float64(v-l.b) / l.r + 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-l.b)/l.r, 0.0, 1.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 index 109d092..43392ce 100644 --- a/lerp/lerp64_test.go +++ b/lerp/lerp64_test.go @@ -261,4 +261,20 @@ func TestLerp64(t *testing.T) { }) }) }) + + 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 index 614d400..047c6e5 100644 --- a/lerp/remap.go +++ b/lerp/remap.go @@ -1,19 +1,13 @@ package lerp -type Remapable interface { - Lerpable -} - -type Remapper[TIn, TOut Remapable, F Float] interface { - Remap(value TIn) TOut - Unmap(value TOut) TIn - MCoeff() F +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 Remapable] Remapper[TIn, TOut, float32] - -type Remapper64[TIn, TOut Remapable] Remapper[TIn, TOut, float64] +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 index cac69df..1f91515 100644 --- a/lerp/remap32.go +++ b/lerp/remap32.go @@ -1,68 +1,39 @@ package lerp -type remap32[TIn, TOut Remapable] struct { - inMin TIn - inMax TIn - outMin TOut - outMax TOut - r float32 +type remap32[TIn, TOut Lerpable] struct { + inLerp Lerper32[TIn] + outLerp Lerper32[TOut] } -func NewRemap32[TIn, TOut Remapable](inMin, inMax TIn, outMin, outMax TOut) Remapper32[TIn, TOut] { - var r float32 - // if rIn is 0, then we don't need to test further, we're always min (max) value - if rIn := float32(inMax) - float32(inMin); rIn != 0 { - if rOut := float32(outMax) - float32(outMin); rOut != 0 { - r = rOut / rIn - } - } +func NewRemap32[TIn, TOut Lerpable](inMin, inMax TIn, outMin, outMax TOut) Remapper32[TIn, TOut] { return remap32[TIn, TOut]{ - inMin: inMin, - inMax: inMax, - outMin: outMin, - outMax: outMax, - r: r, + inLerp: NewLerp32(inMin, inMax), + outLerp: NewLerp32(outMin, outMax), } } func (r remap32[TIn, TOut]) Remap(value TIn) TOut { - switch { - case r.r == 0.0: - return r.outMin - case value == r.inMin: - return r.outMin - case value == r.inMax: - return r.outMax - default: - return r.outMin + TOut(r.r*float32(value-r.inMin)) - } + t := r.inLerp.InverseLerp(value) + return r.outLerp.Lerp(t) } func (r remap32[TIn, TOut]) Unmap(value TOut) TIn { - if r.r == 0.0 { - return r.inMax - } - - rOut := float32(value) - float32(r.outMin) - return r.inMin + TIn(rOut/r.r) -} - -func (r remap32[TIn, TOut]) MCoeff() float32 { - return r.r + t := r.outLerp.InverseLerp(value) + return r.inLerp.Lerp(t) } func (r remap32[TIn, TOut]) InputMinimum() TIn { - return r.inMin + return r.inLerp.OutputMinimum() } func (r remap32[TIn, TOut]) InputMaximum() TIn { - return r.inMax + return r.inLerp.OutputMaximum() } func (r remap32[TIn, TOut]) OutputMinimum() TOut { - return r.outMin + return r.outLerp.OutputMinimum() } func (r remap32[TIn, TOut]) OutputMaximum() TOut { - return r.Remap(r.inMax) + return r.outLerp.OutputMaximum() } diff --git a/lerp/remap32_test.go b/lerp/remap32_test.go index d5109e3..18246c8 100644 --- a/lerp/remap32_test.go +++ b/lerp/remap32_test.go @@ -60,7 +60,8 @@ func TestRemap32(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(12); actual != expected { + // 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) } }) @@ -121,25 +122,6 @@ func TestRemap32(t *testing.T) { }) }) - t.Run("MCoeff", 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 := float32(0.0), l.MCoeff(); actual != expected { - t.Fatalf("Remap32[%v, %v, %v, %v] MCoeff: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual) - } - }) - t.Run("NonZeroRange", 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(outMax-outMin)/float32(inMax-inMin), l.MCoeff(); actual != expected { - t.Fatalf("Remap32[%v, %v, %v, %v] MCoeff: 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) diff --git a/lerp/remap64.go b/lerp/remap64.go index 954910c..69b6907 100644 --- a/lerp/remap64.go +++ b/lerp/remap64.go @@ -1,68 +1,39 @@ package lerp -type remap64[TIn, TOut Remapable] struct { - inMin TIn - inMax TIn - outMin TOut - outMax TOut - r float64 +type remap64[TIn, TOut Lerpable] struct { + inLerp Lerper64[TIn] + outLerp Lerper64[TOut] } -func NewRemap64[TIn, TOut Remapable](inMin, inMax TIn, outMin, outMax TOut) Remapper64[TIn, TOut] { - var r float64 - // if rIn is 0, then we don't need to test further, we're always min (max) value - if rIn := float64(inMax) - float64(inMin); rIn != 0 { - if rOut := float64(outMax) - float64(outMin); rOut != 0 { - r = rOut / rIn - } - } +func NewRemap64[TIn, TOut Lerpable](inMin, inMax TIn, outMin, outMax TOut) Remapper64[TIn, TOut] { return remap64[TIn, TOut]{ - inMin: inMin, - inMax: inMax, - outMin: outMin, - outMax: outMax, - r: r, + inLerp: NewLerp64(inMin, inMax), + outLerp: NewLerp64(outMin, outMax), } } func (r remap64[TIn, TOut]) Remap(value TIn) TOut { - switch { - case r.r == 0.0: - return r.outMin - case value == r.inMin: - return r.outMin - case value == r.inMax: - return r.outMax - default: - return r.outMin + TOut(r.r*float64(value-r.inMin)) - } + t := r.inLerp.InverseLerp(value) + return r.outLerp.Lerp(t) } func (r remap64[TIn, TOut]) Unmap(value TOut) TIn { - if r.r == 0.0 { - return r.inMax - } - - rOut := float64(value) - float64(r.outMin) - return r.inMin + TIn(rOut/r.r) -} - -func (r remap64[TIn, TOut]) MCoeff() float64 { - return r.r + t := r.outLerp.InverseLerp(value) + return r.inLerp.Lerp(t) } func (r remap64[TIn, TOut]) InputMinimum() TIn { - return r.inMin + return r.inLerp.OutputMinimum() } func (r remap64[TIn, TOut]) InputMaximum() TIn { - return r.inMax + return r.inLerp.OutputMaximum() } func (r remap64[TIn, TOut]) OutputMinimum() TOut { - return r.outMin + return r.outLerp.OutputMinimum() } func (r remap64[TIn, TOut]) OutputMaximum() TOut { - return r.Remap(r.inMax) + return r.outLerp.OutputMaximum() } diff --git a/lerp/remap64_test.go b/lerp/remap64_test.go index 3877056..f23a3db 100644 --- a/lerp/remap64_test.go +++ b/lerp/remap64_test.go @@ -102,9 +102,6 @@ func TestRemap64(t *testing.T) { inMin, inMax := 0, 10 outMin, outMax := float64(-math.Pi), float64(math.Pi) l := lerp.NewRemap64(inMin, inMax, outMin, outMax) - // while the 32-bit version of this test truncates down to -1 after some error, - // there's enough information available in a 64-bit float where it properly - // calculates -2 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) } @@ -121,25 +118,6 @@ func TestRemap64(t *testing.T) { }) }) - t.Run("MCoeff", 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 := float64(0.0), l.MCoeff(); actual != expected { - t.Fatalf("Remap64[%v, %v, %v, %v] MCoeff: expected[%v] actual[%v]", inMin, inMax, outMin, outMax, expected, actual) - } - }) - t.Run("NonZeroRange", 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(outMax-outMin)/float64(inMax-inMin), l.MCoeff(); actual != expected { - t.Fatalf("Remap64[%v, %v, %v, %v] MCoeff: 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) diff --git a/lerp/remappoint.go b/lerp/remappoint.go deleted file mode 100644 index 66726c9..0000000 --- a/lerp/remappoint.go +++ /dev/null @@ -1,53 +0,0 @@ -package lerp - -// This is the world's worst Lerp remapper. Regardless of the input value, it always returns the output value. - -type remapPoint[TIn, TOut Remapable, TFloat Float] struct { - in TIn - out TOut -} - -func NewRemapPoint[TIn, TOut Remapable, TFloat Float](in TIn, out TOut) Remapper[TIn, TOut, TFloat] { - return remapPoint[TIn, TOut, TFloat]{ - in: in, - out: out, - } -} - -func NewRemapPoint32[TIn, TOut Remapable](in TIn, out TOut) Remapper[TIn, TOut, float32] { - return NewRemapPoint[TIn, TOut, float32](in, out) -} - -func NewRemapPoint64[TIn, TOut Remapable](in TIn, out TOut) Remapper[TIn, TOut, float64] { - return NewRemapPoint[TIn, TOut, float64](in, out) -} - -func (r remapPoint[TIn, TOut, TFloat]) Remap(value TIn) TOut { - // `value` isn't used here - just return `out` - return r.out -} - -func (r remapPoint[TIn, TOut, TFloat]) Unmap(value TOut) TIn { - // `value` isn't used here - just return `in` - return r.in -} - -func (r remapPoint[TIn, TOut, TFloat]) MCoeff() TFloat { - return 0.0 -} - -func (r remapPoint[TIn, TOut, TFloat]) InputMinimum() TIn { - return r.in -} - -func (r remapPoint[TIn, TOut, TFloat]) InputMaximum() TIn { - return r.in -} - -func (r remapPoint[TIn, TOut, TFloat]) OutputMinimum() TOut { - return r.out -} - -func (r remapPoint[TIn, TOut, TFloat]) OutputMaximum() TOut { - return r.out -} diff --git a/lerp/remappoint_test.go b/lerp/remappoint_test.go deleted file mode 100644 index e7a6a20..0000000 --- a/lerp/remappoint_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package lerp_test - -import ( - "math" - "testing" - - "github.com/awonak/EuroPiGo/lerp" -) - -func TestRemapPoint(t *testing.T) { - t.Run("New", func(t *testing.T) { - t.Run("NewRemapPoint32", func(t *testing.T) { - in, out := 0, float32(math.Pi) - if actual := lerp.NewRemapPoint32(in, out); actual == nil { - t.Fatalf("RemapPoint[%v, %v] NewRemapPoint32: expected[non-nil] actual[nil]", in, out) - } - }) - t.Run("NewRemapPoint64", func(t *testing.T) { - in, out := 0, float32(math.Pi) - if actual := lerp.NewRemapPoint64(in, out); actual == nil { - t.Fatalf("RemapPoint[%v, %v] NewRemapPoint64: expected[non-nil] actual[nil]", in, out) - } - }) - }) - - t.Run("Remap", func(t *testing.T) { - t.Run("ZeroRange", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := out, l.Remap(in); actual != expected { - t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - t.Run("InRange", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := out, l.Remap(in); actual != expected { - t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - - t.Run("OutOfRange", func(t *testing.T) { - t.Run("BelowMin", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := out, l.Remap(-2); actual != expected { - t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - - t.Run("AboveMax", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := out, l.Remap(12); actual != expected { - t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - }) - }) - - t.Run("Unmap", func(t *testing.T) { - t.Run("InRange", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := in, l.Unmap(out); actual != expected { - t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - - t.Run("OutOfRange", func(t *testing.T) { - // Unmap() will work just reply with the "in" point when operating out of range - t.Run("BelowMin", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := in, l.Unmap(out-2); actual != expected { - t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - - t.Run("AboveMax", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := in, l.Unmap(out+2); actual != expected { - t.Fatalf("RemapPoint[%v, %v] Remap: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - }) - }) - - t.Run("MCoeff", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := float32(0.0), l.MCoeff(); actual != expected { - t.Fatalf("RemapPoint[%v, %v] MCoeff: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - - t.Run("InputMinimum", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := in, l.InputMinimum(); actual != expected { - t.Fatalf("RemapPoint[%v, %v] InputMinimum: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - - t.Run("InputMaximum", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := in, l.InputMaximum(); actual != expected { - t.Fatalf("RemapPoint[%v, %v] InputMaximum: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - - t.Run("OutputMinimum", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := out, l.OutputMinimum(); actual != expected { - t.Fatalf("RemapPoint[%v, %v] OutputMinimum: expected[%v] actual[%v]", in, out, expected, actual) - } - }) - - t.Run("OutputMaximum", func(t *testing.T) { - in, out := 0, float32(math.Pi) - l := lerp.NewRemapPoint32(in, out) - if expected, actual := out, l.OutputMaximum(); actual != expected { - t.Fatalf("RemapPoint[%v, %v] OutputMaximum: expected[%v] actual[%v]", in, out, expected, actual) - } - }) -}