From 2b1f6c23f4c10ae08397ceccce66af0a8e62d184 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 7 Oct 2022 01:15:38 -0500 Subject: [PATCH 1/5] refactor EuroPi struct into perferial singletons exposed by the package. --- analog_reader.go | 45 +++++++++++++---------- digital_reader.go | 58 +++++++++++++++++++----------- display.go | 22 +++++++----- europi.go | 42 ++++++++++++++-------- outputer.go | 52 +++++++++++++++++++-------- scripts/clockwerk/clockwerk.go | 66 +++++++++++++++++----------------- 6 files changed, 177 insertions(+), 108 deletions(-) diff --git a/analog_reader.go b/analog_reader.go index d0d4167..f25724b 100644 --- a/analog_reader.go +++ b/analog_reader.go @@ -14,8 +14,17 @@ const ( DefaultSamples = 1000 ) +var ( + AI AnalogReader + K1 AnalogReader + K2 AnalogReader +) + func init() { machine.InitADC() + AI = newAI(machine.ADC0) + K1 = newKnob(machine.ADC1) + K2 = newKnob(machine.ADC2) } // AnalogReader is an interface for common analog read methods for knobs and cv input. @@ -28,39 +37,39 @@ type AnalogReader interface { // 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 { +type analogInput struct { machine.ADC samples uint16 } -// NewAI creates a new AnalogInput. -func NewAI(pin machine.Pin) *AnalogInput { +// 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} + return &analogInput{ADC: adc, samples: DefaultSamples} } // Samples sets the number of reads for an more accurate average read. -func (a *AnalogInput) Samples(samples uint16) { +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 { +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 { +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 { +func (a *analogInput) Range(steps uint16) uint16 { return uint16(a.Percent() * float32(steps)) } -func (a *AnalogInput) read() uint16 { +func (a *analogInput) read() uint16 { var sum int for i := 0; i < int(a.samples); i++ { sum += Clamp(int(a.Get())-CalibratedMinAI, 0, CalibratedMaxAI) @@ -69,39 +78,39 @@ func (a *AnalogInput) read() uint16 { } // A struct for handling the reading of knob voltage and position. -type Knob struct { +type knob struct { machine.ADC samples uint16 } -// NewKnob creates a new Knob struct. -func NewKnob(pin machine.Pin) *Knob { +// 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} + return &knob{ADC: adc, samples: DefaultSamples} } // Samples sets the number of reads for an more accurate average read. -func (k *Knob) Samples(samples uint16) { +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 { +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 { +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 { +func (k *knob) Range(steps uint16) uint16 { return uint16(k.Percent() * float32(steps)) } -func (k *Knob) read() uint16 { +func (k *knob) read() uint16 { var sum int for i := 0; i < int(k.samples); i++ { sum += int(k.Get()) diff --git a/digital_reader.go b/digital_reader.go index eb8bbe5..935b170 100644 --- a/digital_reader.go +++ b/digital_reader.go @@ -5,7 +5,25 @@ import ( "time" ) -const DefaultDebounceDelay = time.Duration(50 * time.Millisecond) +const ( + DefaultDebounceDelay = time.Duration(50 * time.Millisecond) + + DIPin = machine.GPIO22 + B1Pin = machine.GPIO4 + B2Pin = machine.GPIO5 +) + +var ( + DI DigitalReader + B1 DigitalReader + B2 DigitalReader +) + +func init() { + DI = newDI(DIPin) + B1 = newButton(B1Pin) + B2 = newButton(B2Pin) +} // DigitalReader is an interface for common digital inputs methods. type DigitalReader interface { @@ -15,8 +33,8 @@ type DigitalReader interface { Value() bool } -// 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 { Pin machine.Pin debounceDelay time.Duration lastInput time.Time @@ -24,9 +42,9 @@ type DigitalInput struct { } // NewDI creates a new DigitalInput struct. -func NewDI(pin machine.Pin) *DigitalInput { +func newDI(pin machine.Pin) *digitalInput { pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) - return &DigitalInput{ + return &digitalInput{ Pin: pin, lastInput: time.Now(), debounceDelay: DefaultDebounceDelay, @@ -34,29 +52,29 @@ func NewDI(pin machine.Pin) *DigitalInput { } // LastInput return the time of the last high input (triggered at 0.8v). -func (d *DigitalInput) LastInput() time.Time { +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 { +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)) { +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) { +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) { +func (d *digitalInput) debounceWrapper(p machine.Pin) { t := time.Now() if t.Before(d.lastInput.Add(d.debounceDelay)) { return @@ -65,18 +83,18 @@ func (d *DigitalInput) debounceWrapper(p machine.Pin) { d.lastInput = t } -// Button is a struct for handling push button behavior. -type Button struct { +// 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 { +// newButton creates a new Button struct. +func newButton(pin machine.Pin) *button { pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) - return &Button{ + return &button{ Pin: pin, lastInput: time.Now(), debounceDelay: DefaultDebounceDelay, @@ -84,18 +102,18 @@ func NewButton(pin machine.Pin) *Button { } // Handler sets the callback function to be call when the button is pressed. -func (b *Button) Handler(handler func(p machine.Pin)) { +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) { +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) { +func (b *button) debounceWrapper(p machine.Pin) { t := time.Now() if t.Before(b.lastInput.Add(b.debounceDelay)) { return @@ -105,12 +123,12 @@ func (b *Button) debounceWrapper(p machine.Pin) { } // LastInput return the time of the last button press. -func (b *Button) LastInput() time.Time { +func (b *button) LastInput() time.Time { return b.lastInput } // Value returns true if button is currently pressed, else false. -func (b *Button) Value() bool { +func (b *button) Value() bool { // Invert signal to match expected behavior. return !b.Pin.Get() } diff --git a/display.go b/display.go index 15fbad8..d9661d3 100644 --- a/display.go +++ b/display.go @@ -20,37 +20,43 @@ var ( DefaultChannel = machine.I2C0 DefaultFont = &proggy.TinySZ8pt7b White = color.RGBA{255, 255, 255, 255} + + Display *display ) +func init() { + Display = newDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1) +} + // Display is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED. -type Display struct { +type display struct { ssd1306.Device font *tinyfont.Font } -// NewDisplay returns a new Display struct. -func NewDisplay(channel *machine.I2C, sdaPin, sclPin machine.Pin) *Display { +// 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{ + d := ssd1306.NewI2C(DefaultChannel) + d.Configure(ssd1306.Config{ Address: OLEDAddr, Width: OLEDWidth, Height: OLEDHeight, }) - return &Display{Device: display, font: DefaultFont} + return &display{Device: d, font: DefaultFont} } // Font overrides the default font used by `WriteLine`. -func (d *Display) Font(font *tinyfont.Font) { +func (d *display) Font(font *tinyfont.Font) { d.font = font } // WriteLine writes the given text to the display where x, y is the bottom leftmost pixel of the text. -func (d *Display) WriteLine(text string, x, y int16) { +func (d *display) WriteLine(text string, x, y int16) { tinyfont.WriteLine(d, d.font, x, y, text, White) } diff --git a/europi.go b/europi.go index db5a15e..9f78df3 100644 --- a/europi.go +++ b/europi.go @@ -9,10 +9,12 @@ const ( MinVoltage = 0.0 ) +var e *EuroPi + // 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 *display DI DigitalReader AI AnalogReader @@ -32,26 +34,36 @@ type EuroPi struct { CV [6]Outputer } -// New will return a new EuroPi struct. +func init() { + if e == nil { + e = newEuroPi() + } +} + func New() *EuroPi { - cv1 := NewOutput(machine.GPIO21, machine.PWM2) - cv2 := NewOutput(machine.GPIO20, machine.PWM2) - cv3 := NewOutput(machine.GPIO16, machine.PWM0) - cv4 := NewOutput(machine.GPIO17, machine.PWM0) - cv5 := NewOutput(machine.GPIO18, machine.PWM1) - cv6 := NewOutput(machine.GPIO19, machine.PWM1) + return e +} + +// newEuroPi will return a new EuroPi struct. +func newEuroPi() *EuroPi { + cv1 := newOutput(machine.GPIO21, machine.PWM2) + cv2 := newOutput(machine.GPIO20, machine.PWM2) + cv3 := newOutput(machine.GPIO16, machine.PWM0) + cv4 := newOutput(machine.GPIO17, machine.PWM0) + cv5 := newOutput(machine.GPIO18, machine.PWM1) + cv6 := newOutput(machine.GPIO19, machine.PWM1) return &EuroPi{ - Display: NewDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1), + Display: newDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1), - DI: NewDI(machine.GPIO22), - AI: NewAI(machine.ADC0), + DI: newDI(machine.GPIO22), + AI: newAI(machine.ADC0), - B1: NewButton(machine.GPIO4), - B2: NewButton(machine.GPIO5), + B1: newButton(machine.GPIO4), + B2: newButton(machine.GPIO5), - K1: NewKnob(machine.ADC1), - K2: NewKnob(machine.ADC2), + K1: newKnob(machine.ADC1), + K2: newKnob(machine.ADC2), CV1: cv1, CV2: cv2, diff --git a/outputer.go b/outputer.go index 4512bad..105f1ae 100644 --- a/outputer.go +++ b/outputer.go @@ -1,7 +1,7 @@ package europi import ( - "log" + "errors" "machine" ) @@ -13,9 +13,33 @@ const ( CalibratedTop = 0xff - CalibratedOffset ) -// We need a rather high frequency to achieve a stable cv ouput, which means we need a rather low duty cycle period. -// Set a period of 500ns. -var defaultPeriod uint64 = 500 +var ( + CV1 Outputer + CV2 Outputer + CV3 Outputer + CV4 Outputer + CV5 Outputer + CV6 Outputer + CV [6]Outputer +) + +func init() { + 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) + CV = [6]Outputer{CV1, CV2, CV3, CV4, CV5, CV6} +} + +var ( + // 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. + defaultPeriod uint64 = 500 + + ErrInvalidPwmChannel = errors.New("invalid pwm channel") +) // PWMer is an interface for interacting with a machine.pwmGroup type PWMer interface { @@ -37,38 +61,38 @@ type Outputer interface { } // Outputer is struct for interacting with the cv output jacks. -type Output struct { +type output struct { pwm PWMer pin machine.Pin ch uint8 } -// NewOutput returns a new Output struct. -func NewOutput(pin machine.Pin, pwm PWMer) *Output { +// newOutput returns a new Output struct. +func newOutput(pin machine.Pin, pwm PWMer) *output { err := pwm.Configure(machine.PWMConfig{ Period: defaultPeriod, }) if err != nil { - log.Fatal("pwm Configure error: ", err.Error()) + panic("pwm Configure error") } pwm.SetTop(CalibratedTop) ch, err := pwm.Channel(pin) if err != nil { - log.Fatal("pwm Channel error: ", err.Error()) + panic("pwm Channel 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) { +func (o *output) Voltage(v float32) { v = Clamp(v, MinVoltage, MaxVoltage) invertedCv := (v / MaxVoltage) * float32(o.pwm.Top()) // cv := (float32(o.pwm.Top()) - invertedCv) - CalibratedOffset @@ -77,11 +101,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/scripts/clockwerk/clockwerk.go b/scripts/clockwerk/clockwerk.go index 217158c..8580b25 100644 --- a/scripts/clockwerk/clockwerk.go +++ b/scripts/clockwerk/clockwerk.go @@ -74,8 +74,6 @@ type Clockwerk struct { period time.Duration clocks [6]int resets [6]chan uint8 - - *europi.EuroPi } func (c *Clockwerk) editParams() { @@ -95,7 +93,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 := europi.K1.Range((MaxBPM+1)-(MinBPM-2)) + MinBPM - 1 if _bpm < MinBPM { c.external = true _bpm = 0 @@ -110,7 +108,7 @@ func (c *Clockwerk) readBPM() uint16 { } func (c *Clockwerk) readFactor() int { - return FactorChoices[c.K2.Range(uint16(len(FactorChoices)))] + return FactorChoices[europi.K2.Range(uint16(len(FactorChoices)))] } func (c *Clockwerk) startClocks() { @@ -152,11 +150,11 @@ func (c *Clockwerk) clock(i uint8, reset chan uint8) { high, low := c.clockPulseWidth(c.clocks[i]) - c.CV[i].On() + europi.CV[i].On() t = t.Add(high) time.Sleep(t.Sub(time.Now())) - c.CV[i].Off() + europi.CV[i].Off() t = t.Add(low) time.Sleep(t.Sub(time.Now())) } @@ -185,14 +183,14 @@ func (c *Clockwerk) updateDisplay() { return } c.displayShouldUpdate = false - c.Display.ClearBuffer() + europi.Display.ClearBuffer() // Master clock and pulse width. var external string if c.external { external = "^" } - c.Display.WriteLine(external+"BPM: "+strconv.Itoa(int(c.bpm)), 2, 8) + europi.Display.WriteLine(external+"BPM: "+strconv.Itoa(int(c.bpm)), 2, 8) // Display each clock multiplication or division setting. for i, factor := range c.clocks { @@ -203,57 +201,59 @@ 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) + europi.Display.WriteLine(text, int16(i*europi.OLEDWidth/len(c.clocks))+2, 26) } xWidth := int16(europi.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(europi.Display, xOffset, 16, xWidth, 16, europi.White) - c.Display.Display() + europi.Display.Display() +} + +func (c *Clockwerk) moveSelectedFactor(pin machine.Pin) { + var move int + switch pin { + case europi.B1Pin: + move = -1 + case europi.B2Pin: + move = 1 + } + if europi.B1.Value() && europi.B2.Value() { + c.doClockReset = true + return + } + c.selected = uint8(europi.Clamp(int(c.selected)+move, 0, len(c.clocks)-1)) + c.displayShouldUpdate = true } func main() { c := Clockwerk{ - EuroPi: europi.New(), clocks: DefaultFactor, displayShouldUpdate: true, } // Lower range value can have lower sample size - c.K1.Samples(500) - c.K2.Samples(20) + europi.K1.Samples(500) + europi.K2.Samples(20) - c.DI.Handler(func(pin machine.Pin) { + europi.DI.Handler(func(pin machine.Pin) { // Measure current period between clock pulses. - c.period = time.Now().Sub(c.DI.LastInput()) + c.period = time.Now().Sub(europi.DI.LastInput()) }) // Move clock config option to the left. - c.B1.Handler(func(p machine.Pin) { - if c.B2.Value() { - c.doClockReset = true - return - } - c.selected = uint8(europi.Clamp(int(c.selected)-1, 0, len(c.clocks))) - c.displayShouldUpdate = true - }) + europi.B1.Handler(c.moveSelectedFactor) // Move clock config option to the right. - c.B2.Handler(func(p machine.Pin) { - if c.B1.Value() { - c.doClockReset = true - return - } - c.selected = uint8(europi.Clamp(int(c.selected)+1, 0, len(c.clocks)-1)) - c.displayShouldUpdate = true - }) + europi.B2.Handler(c.moveSelectedFactor) // Init parameter configs based on current knob positions. c.bpm = c.readBPM() c.prevk2 = c.readFactor() c.startClocks() + europi.DebugMemoryUsedPerSecond() for { // Check for clock updates every 2 seconds. @@ -263,6 +263,6 @@ func main() { c.resetClocks() c.displayShouldUpdate = true } - europi.DebugMemoryUsage() + // europi.DebugMemoryUsage() } } From a533d4bd5f674c1a755ebbca5f88c410e5d07fdd Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 7 Oct 2022 08:43:40 -0500 Subject: [PATCH 2/5] Define pins in main europi package file. --- analog_reader.go | 6 +-- digital_reader.go | 8 +-- display.go | 2 +- europi.go | 86 +++++++++--------------------- outputer.go | 12 ++--- scripts/diagnostics/diagnostics.go | 50 +++++++++-------- 6 files changed, 60 insertions(+), 104 deletions(-) diff --git a/analog_reader.go b/analog_reader.go index f25724b..839c69d 100644 --- a/analog_reader.go +++ b/analog_reader.go @@ -22,9 +22,9 @@ var ( func init() { machine.InitADC() - AI = newAI(machine.ADC0) - K1 = newKnob(machine.ADC1) - K2 = newKnob(machine.ADC2) + AI = newAI(AIPin) + K1 = newKnob(K1Pin) + K2 = newKnob(K2Pin) } // AnalogReader is an interface for common analog read methods for knobs and cv input. diff --git a/digital_reader.go b/digital_reader.go index 935b170..d6599a5 100644 --- a/digital_reader.go +++ b/digital_reader.go @@ -5,13 +5,7 @@ import ( "time" ) -const ( - DefaultDebounceDelay = time.Duration(50 * time.Millisecond) - - DIPin = machine.GPIO22 - B1Pin = machine.GPIO4 - B2Pin = machine.GPIO5 -) +const DefaultDebounceDelay = time.Duration(50 * time.Millisecond) var ( DI DigitalReader diff --git a/display.go b/display.go index d9661d3..4a13b84 100644 --- a/display.go +++ b/display.go @@ -25,7 +25,7 @@ var ( ) func init() { - Display = newDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1) + Display = newDisplay(DisplayChannel, DisplaySdaPin, DisplaySclPin) } // Display is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED. diff --git a/europi.go b/europi.go index 9f78df3..d5e32b1 100644 --- a/europi.go +++ b/europi.go @@ -7,70 +7,34 @@ import ( const ( MaxVoltage = 10.0 MinVoltage = 0.0 -) - -var e *EuroPi - -// 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 - - DI DigitalReader - AI AnalogReader - - B1 DigitalReader - B2 DigitalReader - - K1 AnalogReader - K2 AnalogReader - CV1 Outputer - CV2 Outputer - CV3 Outputer - CV4 Outputer - CV5 Outputer - CV6 Outputer - CV [6]Outputer -} + // EuroPi hardware GPIO pins + DIPin = machine.GPIO22 + AIPin = machine.ADC0 -func init() { - if e == nil { - e = newEuroPi() - } -} + DisplaySdaPin = machine.GPIO0 + DisplaySclPin = machine.GPIO1 -func New() *EuroPi { - return e -} + K1Pin = machine.ADC1 + K2Pin = machine.ADC2 -// newEuroPi will return a new EuroPi struct. -func newEuroPi() *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) + B1Pin = machine.GPIO4 + B2Pin = machine.GPIO5 - return &EuroPi{ - Display: newDisplay(machine.I2C0, machine.GPIO0, machine.GPIO1), - - DI: newDI(machine.GPIO22), - AI: newAI(machine.ADC0), - - B1: newButton(machine.GPIO4), - B2: newButton(machine.GPIO5), - - K1: newKnob(machine.ADC1), - K2: newKnob(machine.ADC2), + CV1Pin = machine.GPIO21 + CV2Pin = machine.GPIO20 + CV3Pin = machine.GPIO16 + CV4Pin = machine.GPIO17 + CV5Pin = machine.GPIO18 + CV6Pin = machine.GPIO19 +) - CV1: cv1, - CV2: cv2, - CV3: cv3, - CV4: cv4, - CV5: cv5, - CV6: cv5, - CV: [6]Outputer{cv1, cv2, cv3, cv4, cv5, cv6}, - } -} +var ( + DisplayChannel = machine.I2C0 + CV1PwmGroup = machine.PWM2 + CV2PwmGroup = machine.PWM2 + CV3PwmGroup = machine.PWM0 + CV4PwmGroup = machine.PWM0 + CV5PwmGroup = machine.PWM1 + CV6PwmGroup = machine.PWM1 +) diff --git a/outputer.go b/outputer.go index 105f1ae..5adefd0 100644 --- a/outputer.go +++ b/outputer.go @@ -24,12 +24,12 @@ var ( ) func init() { - 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 = newOutput(CV1Pin, CV1PwmGroup) + CV2 = newOutput(CV2Pin, CV2PwmGroup) + CV3 = newOutput(CV3Pin, CV3PwmGroup) + CV4 = newOutput(CV4Pin, CV4PwmGroup) + CV5 = newOutput(CV5Pin, CV5PwmGroup) + CV6 = newOutput(CV6Pin, CV6PwmGroup) CV = [6]Outputer{CV1, CV2, CV3, CV4, CV5, CV6} } diff --git a/scripts/diagnostics/diagnostics.go b/scripts/diagnostics/diagnostics.go index f6c1043..bad1ab5 100644 --- a/scripts/diagnostics/diagnostics.go +++ b/scripts/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 EuroPiGo firmwareuropi. package main import ( @@ -24,55 +24,53 @@ func main() { staticCv: 5, } - e := europi.New() - // Demonstrate adding a IRQ handler to B1 and B2. - e.B1.Handler(func(p machine.Pin) { + europi.B1.Handler(func(p machine.Pin) { myApp.knobsDisplayPercent = !myApp.knobsDisplayPercent }) - e.B2.Handler(func(p machine.Pin) { + europi.B2.Handler(func(p machine.Pin) { myApp.staticCv = (myApp.staticCv + 1) % europi.MaxVoltage }) for { - e.Display.ClearBuffer() + europi.Display.ClearBuffer() // Highlight the border of the oled display. - tinydraw.Rectangle(e.Display, 0, 0, 128, 32, europi.White) + tinydraw.Rectangle(europi.Display, 0, 0, 128, 32, europi.White) // Display analog and digital input values. - inputText := fmt.Sprintf("din: %5v ain: %2.2f ", e.DI.Value(), e.AI.Percent()) - e.Display.WriteLine(inputText, 3, 8) + inputText := fmt.Sprintf("din: %5v ain: %2.2f ", europi.DI.Value(), europi.AI.Percent()) + europi.Display.WriteLine(inputText, 3, 8) - // Display knob values based on app state. + // Display knob values based on app stateuropi. var knobText string if myApp.knobsDisplayPercent { - knobText = fmt.Sprintf("K1: %0.2f K2: %0.2f", e.K1.Percent(), e.K2.Percent()) + knobText = fmt.Sprintf("K1: %0.2f K2: %0.2f", europi.K1.Percent(), europi.K2.Percent()) } else { - knobText = fmt.Sprintf("K1: %2d K2: %2d", e.K1.Range(100), e.K2.Range(100)) + knobText = fmt.Sprintf("K1: %2d K2: %2d", europi.K1.Range(100), europi.K2.Range(100)) } - e.Display.WriteLine(knobText, 3, 18) + europi.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) + // Show current button press stateuropi. + europi.Display.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", europi.B1.Value(), europi.B2.Value()), 3, 28) - e.Display.Display() + europi.Display.Display() // Set voltage values for the 6 CV outputs. - if e.K1.Range(1<<12) != myApp.prevK1 { - e.CV1.Voltage(e.K1.ReadVoltage()) - e.CV4.Voltage(europi.MaxVoltage - e.K1.ReadVoltage()) - myApp.prevK1 = e.K1.Range(1 << 12) + if europi.K1.Range(1<<12) != myApp.prevK1 { + europi.CV1.Voltage(europi.K1.ReadVoltage()) + europi.CV4.Voltage(europi.MaxVoltage - europi.K1.ReadVoltage()) + myApp.prevK1 = europi.K1.Range(1 << 12) } - if e.K2.Range(1<<12) != myApp.prevK2 { - e.CV2.Voltage(e.K2.ReadVoltage()) - e.CV5.Voltage(europi.MaxVoltage - e.K2.ReadVoltage()) - myApp.prevK2 = e.K2.Range(1 << 12) + if europi.K2.Range(1<<12) != myApp.prevK2 { + europi.CV2.Voltage(europi.K2.ReadVoltage()) + europi.CV5.Voltage(europi.MaxVoltage - europi.K2.ReadVoltage()) + myApp.prevK2 = europi.K2.Range(1 << 12) } - e.CV3.On() + europi.CV3.On() if myApp.staticCv != myApp.prevStaticCv { - e.CV6.Voltage(float32(myApp.staticCv)) + europi.CV6.Voltage(float32(myApp.staticCv)) myApp.prevStaticCv = myApp.staticCv } } From 53750c818cdf6bda564df1a06d2e65115a1a4a1a Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 13 Oct 2022 23:09:59 -0500 Subject: [PATCH 3/5] big honkin refactor to better use interfaces and provide a proper EuroPi singleton. --- analog_reader.go | 90 ++++++++++----------------- digital_reader.go | 98 +++++++++--------------------- display.go | 16 ++--- europi.go | 73 +++++++++++++++++++++- outputer.go | 46 +++----------- scripts/clockwerk/clockwerk.go | 35 +++++------ scripts/diagnostics/diagnostics.go | 93 +++++++++++++++++----------- 7 files changed, 219 insertions(+), 232 deletions(-) diff --git a/analog_reader.go b/analog_reader.go index 839c69d..7b65e41 100644 --- a/analog_reader.go +++ b/analog_reader.go @@ -2,29 +2,20 @@ package europi import ( "machine" - "math" ) const ( + // Default number of analog reads to average over. + defaultSamples = 32 + // 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 -) - -var ( - AI AnalogReader - K1 AnalogReader - K2 AnalogReader + calibratedMinAI = 300 + calibratedMaxAI = 44009 ) func init() { machine.InitADC() - AI = newAI(AIPin) - K1 = newKnob(K1Pin) - K2 = newKnob(K2Pin) } // AnalogReader is an interface for common analog read methods for knobs and cv input. @@ -33,87 +24,68 @@ type AnalogReader interface { ReadVoltage() float32 Percent() float32 Range(steps uint16) uint16 + read() 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 { +type analogReader struct { machine.ADC + samples uint16 } -// newAI creates a new AnalogInput. -func newAI(pin machine.Pin) *analogInput { +func newAnalogReader(pin machine.Pin) *analogReader { adc := machine.ADC{Pin: pin} adc.Configure(machine.ADCConfig{}) - return &analogInput{ADC: adc, samples: DefaultSamples} + return &analogReader{ + ADC: adc, + samples: defaultSamples, + } } // Samples sets the number of reads for an more accurate average read. -func (a *analogInput) Samples(samples uint16) { +func (a *analogReader) 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 +func (a *analogReader) 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 { +func (a *analogReader) 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 { +func (a *analogReader) Range(steps uint16) uint16 { return uint16(a.Percent() * float32(steps)) } -func (a *analogInput) read() uint16 { +func (a *analogReader) read() uint16 { var sum int for i := 0; i < int(a.samples); i++ { - sum += Clamp(int(a.Get())-CalibratedMinAI, 0, CalibratedMaxAI) + 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 +type analogInput struct { + AnalogReader } -// ReadVoltage return the current read voltage between 0.0 and 10.0 volts. -func (k *knob) ReadVoltage() float32 { - return k.Percent() * MaxVoltage +func newAnalogInput(pin machine.Pin) *analogInput { + return &analogInput{ + newAnalogReader(pin), + } } -// 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)) +type knob struct { + AnalogReader } -func (k *knob) read() uint16 { - var sum int - for i := 0; i < int(k.samples); i++ { - sum += int(k.Get()) +func newKnob(pin machine.Pin) *knob { + return &knob{ + newAnalogReader(pin), } - return uint16(sum / int(k.samples)) } diff --git a/digital_reader.go b/digital_reader.go index d6599a5..feb4a22 100644 --- a/digital_reader.go +++ b/digital_reader.go @@ -5,70 +5,59 @@ import ( "time" ) -const DefaultDebounceDelay = time.Duration(50 * time.Millisecond) +const defaultDebounceDelay = time.Duration(50 * time.Millisecond) -var ( - DI DigitalReader - B1 DigitalReader - B2 DigitalReader -) - -func init() { - DI = newDI(DIPin) - B1 = newButton(B1Pin) - B2 = newButton(B2Pin) -} +type callback func(machine.Pin) // DigitalReader is an interface for common digital inputs methods. type DigitalReader interface { - Handler(func(machine.Pin)) - HandlerWithDebounce(func(machine.Pin), time.Duration) + Handler(callback) + HandlerWithDebounce(callback, time.Duration) LastInput() time.Time Value() bool } -// digitalInput is a struct for handling reading of the digital input. -type digitalInput struct { - Pin machine.Pin +type digitalReader struct { + machine.Pin + debounceDelay time.Duration lastInput time.Time - callback func(p machine.Pin) + callback callback } -// NewDI creates a new DigitalInput struct. -func newDI(pin machine.Pin) *digitalInput { +func newDigitalReader(pin machine.Pin) *digitalReader { pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) - return &digitalInput{ + return &digitalReader{ Pin: pin, lastInput: time.Now(), - debounceDelay: DefaultDebounceDelay, + debounceDelay: defaultDebounceDelay, } } // LastInput return the time of the last high input (triggered at 0.8v). -func (d *digitalInput) LastInput() time.Time { +func (d *digitalReader) LastInput() time.Time { return d.lastInput } // Value returns true if the input is high (above 0.8v), else false. -func (d *digitalInput) Value() bool { +func (d *digitalReader) 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) +func (d *digitalReader) Handler(c callback) { + d.HandlerWithDebounce(c, 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 +func (d *digitalReader) HandlerWithDebounce(c callback, delay time.Duration) { + d.callback = c d.debounceDelay = delay d.Pin.SetInterrupt(machine.PinFalling, d.debounceWrapper) } -func (d *digitalInput) debounceWrapper(p machine.Pin) { +func (d *digitalReader) debounceWrapper(p machine.Pin) { t := time.Now() if t.Before(d.lastInput.Add(d.debounceDelay)) { return @@ -77,52 +66,23 @@ func (d *digitalInput) debounceWrapper(p machine.Pin) { 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) +type digitalInput struct { + DigitalReader } -// 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, +func newDigitalInput(pin machine.Pin) *digitalInput { + return &digitalInput{ + newDigitalReader(pin), } } -// 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) +type button struct { + DigitalReader } -func (b *button) debounceWrapper(p machine.Pin) { - t := time.Now() - if t.Before(b.lastInput.Add(b.debounceDelay)) { - return +func newButton(pin machine.Pin) *button { + pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) + return &button{ + newDigitalReader(pin), } - b.callback(p) - b.lastInput = t -} - -// LastInput return the time of the last button press. -func (b *button) LastInput() time.Time { - return b.lastInput -} - -// Value returns true if button is currently pressed, else false. -func (b *button) Value() bool { - // Invert signal to match expected behavior. - return !b.Pin.Get() } diff --git a/display.go b/display.go index 4a13b84..fb6c126 100644 --- a/display.go +++ b/display.go @@ -17,24 +17,16 @@ const ( ) var ( - DefaultChannel = machine.I2C0 - DefaultFont = &proggy.TinySZ8pt7b - White = color.RGBA{255, 255, 255, 255} - - Display *display + DefaultFont = &proggy.TinySZ8pt7b + White = color.RGBA{255, 255, 255, 255} ) -func init() { - Display = newDisplay(DisplayChannel, DisplaySdaPin, DisplaySclPin) -} - -// 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, @@ -42,7 +34,7 @@ func newDisplay(channel *machine.I2C, sdaPin, sclPin machine.Pin) *display { SCL: sclPin, }) - d := ssd1306.NewI2C(DefaultChannel) + d := ssd1306.NewI2C(channel) d.Configure(ssd1306.Config{ Address: OLEDAddr, Width: OLEDWidth, diff --git a/europi.go b/europi.go index d5e32b1..3cbee9a 100644 --- a/europi.go +++ b/europi.go @@ -5,10 +5,11 @@ import ( ) const ( + // EuroPi voltage range constants. MaxVoltage = 10.0 MinVoltage = 0.0 - // EuroPi hardware GPIO pins + // EuroPi hardware GPIO pins. DIPin = machine.GPIO22 AIPin = machine.ADC0 @@ -37,4 +38,74 @@ var ( CV4PwmGroup = machine.PWM0 CV5PwmGroup = machine.PWM1 CV6PwmGroup = machine.PWM1 + + europi *EuroPi ) + +// EuroPi is the collection of component wrappers used to interact with the module. +type EuroPi struct { + // Display provides methods for drawing to the OLED display. + Display *display + + DI *digitalInput + // AI provides methods for reading analog input control voltage between 0 and 12V. + AI *analogInput + + // B1 is a struct for handling the left push button behavior. + B1 *button + // B2 is a struct for handling the right push button behavior. + B2 *button + + // K1 provides methods for reading knob voltage and position for the left knob. + K1 *knob + // K2 provides methods for reading knob voltage and position for the left knob. + K2 *knob + + // CV1-6 are structs for interacting with the cv output jacks. + CV1 *output + CV2 *output + CV3 *output + CV4 *output + CV5 *output + CV6 *output + // CV is an array containing all CV outputs. + CV [6]*output +} + +func init() { + europi = new() +} + +func new() *EuroPi { + cv1 := newOutput(CV1Pin, CV1PwmGroup) + cv2 := newOutput(CV2Pin, CV2PwmGroup) + cv3 := newOutput(CV3Pin, CV3PwmGroup) + cv4 := newOutput(CV4Pin, CV4PwmGroup) + cv5 := newOutput(CV5Pin, CV5PwmGroup) + cv6 := newOutput(CV6Pin, CV6PwmGroup) + + return &EuroPi{ + Display: newDisplay(DisplayChannel, DisplaySdaPin, DisplaySclPin), + + DI: newDigitalInput(DIPin), + AI: newAnalogInput(AIPin), + + B1: newButton(B1Pin), + B2: newButton(B2Pin), + + K1: newKnob(K1Pin), + K2: newKnob(K2Pin), + + CV1: cv1, + CV2: cv2, + CV3: cv3, + CV4: cv4, + CV5: cv5, + CV6: cv5, + CV: [6]*output{cv1, cv2, cv3, cv4, cv5, cv6}, + } +} + +func GetInstance() *EuroPi { + return europi +} diff --git a/outputer.go b/outputer.go index 5adefd0..501c8a2 100644 --- a/outputer.go +++ b/outputer.go @@ -1,44 +1,21 @@ package europi import ( - "errors" "machine" ) const ( // Manually calibrated to best match expected voltages. Additional info: // https://github.com/Allen-Synthesis/EuroPi/blob/main/software/programming_instructions.md#calibrate-the-module - CalibratedOffset = 0 + 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 + calibratedTop = 0xff - calibratedOffset ) -var ( - CV1 Outputer - CV2 Outputer - CV3 Outputer - CV4 Outputer - CV5 Outputer - CV6 Outputer - CV [6]Outputer -) - -func init() { - CV1 = newOutput(CV1Pin, CV1PwmGroup) - CV2 = newOutput(CV2Pin, CV2PwmGroup) - CV3 = newOutput(CV3Pin, CV3PwmGroup) - CV4 = newOutput(CV4Pin, CV4PwmGroup) - CV5 = newOutput(CV5Pin, CV5PwmGroup) - CV6 = newOutput(CV6Pin, CV6PwmGroup) - CV = [6]Outputer{CV1, CV2, CV3, CV4, CV5, CV6} -} - var ( // 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. defaultPeriod uint64 = 500 - - ErrInvalidPwmChannel = errors.New("invalid pwm channel") ) // PWMer is an interface for interacting with a machine.pwmGroup @@ -52,35 +29,26 @@ type PWMer interface { SetPeriod(period uint64) error } -// Outputer is an interface for interacting with the cv output jacks. -type Outputer interface { - Get() (value uint32) - Voltage(v float32) - On() - Off() -} - -// Outputer is struct for interacting with the cv output jacks. +// output is struct for interacting with the cv output jacks. type output struct { pwm PWMer pin machine.Pin ch uint8 } -// newOutput returns a new Output struct. func newOutput(pin machine.Pin, pwm PWMer) *output { err := pwm.Configure(machine.PWMConfig{ Period: defaultPeriod, }) if err != nil { - panic("pwm Configure error") + panic("PWM Configure error") } - pwm.SetTop(CalibratedTop) + pwm.SetTop(calibratedTop) ch, err := pwm.Channel(pin) if err != nil { - panic("pwm Channel error") + panic("PWM Channel error") } return &output{pwm, pin, ch} @@ -96,7 +64,7 @@ func (o *output) Voltage(v float32) { v = Clamp(v, MinVoltage, MaxVoltage) invertedCv := (v / MaxVoltage) * float32(o.pwm.Top()) // cv := (float32(o.pwm.Top()) - invertedCv) - CalibratedOffset - cv := float32(invertedCv) - CalibratedOffset + cv := float32(invertedCv) - calibratedOffset o.pwm.Set(o.ch, uint32(cv)) } diff --git a/scripts/clockwerk/clockwerk.go b/scripts/clockwerk/clockwerk.go index 8580b25..48bbc1f 100644 --- a/scripts/clockwerk/clockwerk.go +++ b/scripts/clockwerk/clockwerk.go @@ -49,6 +49,7 @@ var ( // Positive values are multiplications and negative values are divisions. DefaultFactor = [6]int{1, 2, 4, -2, -4, -8} FactorChoices []int + EuroPi = europi.GetInstance() ) func init() { @@ -93,7 +94,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 := europi.K1.Range((MaxBPM+1)-(MinBPM-2)) + MinBPM - 1 + _bpm := EuroPi.K1.Range((MaxBPM+1)-(MinBPM-2)) + MinBPM - 1 if _bpm < MinBPM { c.external = true _bpm = 0 @@ -108,7 +109,7 @@ func (c *Clockwerk) readBPM() uint16 { } func (c *Clockwerk) readFactor() int { - return FactorChoices[europi.K2.Range(uint16(len(FactorChoices)))] + return FactorChoices[EuroPi.K2.Range(uint16(len(FactorChoices)))] } func (c *Clockwerk) startClocks() { @@ -150,11 +151,11 @@ func (c *Clockwerk) clock(i uint8, reset chan uint8) { high, low := c.clockPulseWidth(c.clocks[i]) - europi.CV[i].On() + EuroPi.CV[i].On() t = t.Add(high) time.Sleep(t.Sub(time.Now())) - europi.CV[i].Off() + EuroPi.CV[i].Off() t = t.Add(low) time.Sleep(t.Sub(time.Now())) } @@ -183,14 +184,14 @@ func (c *Clockwerk) updateDisplay() { return } c.displayShouldUpdate = false - europi.Display.ClearBuffer() + EuroPi.Display.ClearBuffer() // Master clock and pulse width. var external string if c.external { external = "^" } - europi.Display.WriteLine(external+"BPM: "+strconv.Itoa(int(c.bpm)), 2, 8) + EuroPi.Display.WriteLine(external+"BPM: "+strconv.Itoa(int(c.bpm)), 2, 8) // Display each clock multiplication or division setting. for i, factor := range c.clocks { @@ -201,14 +202,14 @@ func (c *Clockwerk) updateDisplay() { case factor > 1: text = "x" + strconv.Itoa(factor) } - europi.Display.WriteLine(text, int16(i*europi.OLEDWidth/len(c.clocks))+2, 26) + EuroPi.Display.WriteLine(text, int16(i*europi.OLEDWidth/len(c.clocks))+2, 26) } xWidth := int16(europi.OLEDWidth / len(c.clocks)) xOffset := int16(c.selected) * xWidth // TODO: replace box with chevron. - tinydraw.Rectangle(europi.Display, xOffset, 16, xWidth, 16, europi.White) + tinydraw.Rectangle(EuroPi.Display, xOffset, 16, xWidth, 16, europi.White) - europi.Display.Display() + EuroPi.Display.Display() } func (c *Clockwerk) moveSelectedFactor(pin machine.Pin) { @@ -219,7 +220,7 @@ func (c *Clockwerk) moveSelectedFactor(pin machine.Pin) { case europi.B2Pin: move = 1 } - if europi.B1.Value() && europi.B2.Value() { + if EuroPi.B1.Value() && EuroPi.B2.Value() { c.doClockReset = true return } @@ -234,26 +235,26 @@ func main() { } // Lower range value can have lower sample size - europi.K1.Samples(500) - europi.K2.Samples(20) + EuroPi.K1.Samples(500) + EuroPi.K2.Samples(20) - europi.DI.Handler(func(pin machine.Pin) { + EuroPi.DI.Handler(func(pin machine.Pin) { // Measure current period between clock pulses. - c.period = time.Now().Sub(europi.DI.LastInput()) + c.period = time.Now().Sub(EuroPi.DI.LastInput()) }) // Move clock config option to the left. - europi.B1.Handler(c.moveSelectedFactor) + EuroPi.B1.Handler(c.moveSelectedFactor) // Move clock config option to the right. - europi.B2.Handler(c.moveSelectedFactor) + EuroPi.B2.Handler(c.moveSelectedFactor) // Init parameter configs based on current knob positions. c.bpm = c.readBPM() c.prevk2 = c.readFactor() c.startClocks() - europi.DebugMemoryUsedPerSecond() + go europi.DebugMemoryUsedPerSecond() for { // Check for clock updates every 2 seconds. diff --git a/scripts/diagnostics/diagnostics.go b/scripts/diagnostics/diagnostics.go index bad1ab5..66d5ca0 100644 --- a/scripts/diagnostics/diagnostics.go +++ b/scripts/diagnostics/diagnostics.go @@ -1,9 +1,10 @@ -// Diagnostics is a script for demonstrating all main interactions with the EuroPiGo firmwareuropi. +// Diagnostics is a script for demonstrating all main interactions with the EuroPiGo firmware. package main import ( "fmt" "machine" + "time" "tinygo.org/x/tinydraw" @@ -12,66 +13,88 @@ import ( type MyApp struct { knobsDisplayPercent bool + displayShouldUpdate bool prevK1 uint16 prevK2 uint16 staticCv int prevStaticCv int } +func (m *MyApp) updateDisplay() { + if !m.displayShouldUpdate { + return + } + m.displayShouldUpdate = false + + EuroPi := europi.GetInstance() + + EuroPi.Display.ClearBuffer() + + // Highlight the border of the oled display. + tinydraw.Rectangle(EuroPi.Display, 0, 0, 128, 32, europi.White) + + // Display analog and digital input values. + inputText := fmt.Sprintf("din: %5v ain: %2.2f ", EuroPi.DI.Value(), EuroPi.AI.Percent()) + EuroPi.Display.WriteLine(inputText, 3, 8) + + // Display knob values based on app stateuropi. + var knobText string + if m.knobsDisplayPercent { + knobText = fmt.Sprintf("K1: %0.2f K2: %0.2f", EuroPi.K1.Percent(), EuroPi.K2.Percent()) + } else { + knobText = fmt.Sprintf("K1: %2d K2: %2d", EuroPi.K1.Range(100), EuroPi.K2.Range(100)) + } + EuroPi.Display.WriteLine(knobText, 3, 18) + + // Show current button press stateuropi. + EuroPi.Display.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", EuroPi.B1.Value(), EuroPi.B2.Value()), 3, 28) + + EuroPi.Display.Display() +} func main() { myApp := MyApp{ staticCv: 5, } + EuroPi := europi.GetInstance() // Demonstrate adding a IRQ handler to B1 and B2. - europi.B1.Handler(func(p machine.Pin) { + EuroPi.B1.Handler(func(p machine.Pin) { myApp.knobsDisplayPercent = !myApp.knobsDisplayPercent + myApp.displayShouldUpdate = true }) - europi.B2.Handler(func(p machine.Pin) { + EuroPi.B2.Handler(func(p machine.Pin) { myApp.staticCv = (myApp.staticCv + 1) % europi.MaxVoltage - }) - - for { - europi.Display.ClearBuffer() - - // Highlight the border of the oled display. - tinydraw.Rectangle(europi.Display, 0, 0, 128, 32, europi.White) - - // Display analog and digital input values. - inputText := fmt.Sprintf("din: %5v ain: %2.2f ", europi.DI.Value(), europi.AI.Percent()) - europi.Display.WriteLine(inputText, 3, 8) + myApp.displayShouldUpdate = true - // Display knob values based on app stateuropi. - var knobText string - if myApp.knobsDisplayPercent { - knobText = fmt.Sprintf("K1: %0.2f K2: %0.2f", europi.K1.Percent(), europi.K2.Percent()) - } else { - knobText = fmt.Sprintf("K1: %2d K2: %2d", europi.K1.Range(100), europi.K2.Range(100)) - } - europi.Display.WriteLine(knobText, 3, 18) + }) - // Show current button press stateuropi. - europi.Display.WriteLine(fmt.Sprintf("B1: %5v B2: %5v", europi.B1.Value(), europi.B2.Value()), 3, 28) + go europi.DebugMemoryUsedPerSecond() - europi.Display.Display() + for { // Set voltage values for the 6 CV outputs. - if europi.K1.Range(1<<12) != myApp.prevK1 { - europi.CV1.Voltage(europi.K1.ReadVoltage()) - europi.CV4.Voltage(europi.MaxVoltage - europi.K1.ReadVoltage()) - myApp.prevK1 = europi.K1.Range(1 << 12) + if EuroPi.K1.Range(1<<12) != myApp.prevK1 { + EuroPi.CV1.Voltage(EuroPi.K1.ReadVoltage()) + EuroPi.CV4.Voltage(europi.MaxVoltage - EuroPi.K1.ReadVoltage()) + myApp.prevK1 = EuroPi.K1.Range(1 << 12) + myApp.displayShouldUpdate = true } - if europi.K2.Range(1<<12) != myApp.prevK2 { - europi.CV2.Voltage(europi.K2.ReadVoltage()) - europi.CV5.Voltage(europi.MaxVoltage - europi.K2.ReadVoltage()) - myApp.prevK2 = europi.K2.Range(1 << 12) + if EuroPi.K2.Range(1<<12) != myApp.prevK2 { + EuroPi.CV2.Voltage(EuroPi.K2.ReadVoltage()) + EuroPi.CV5.Voltage(europi.MaxVoltage - EuroPi.K2.ReadVoltage()) + myApp.prevK2 = EuroPi.K2.Range(1 << 12) + myApp.displayShouldUpdate = true } - europi.CV3.On() + EuroPi.CV3.On() if myApp.staticCv != myApp.prevStaticCv { - europi.CV6.Voltage(float32(myApp.staticCv)) + EuroPi.CV6.Voltage(float32(myApp.staticCv)) myApp.prevStaticCv = myApp.staticCv + myApp.displayShouldUpdate = true } + + myApp.updateDisplay() + time.Sleep(10 * time.Millisecond) } } From 21503963714aa426e2d187a7238fad520894383b Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 14 Oct 2022 08:26:15 -0500 Subject: [PATCH 4/5] the underlying hardware for analog input and knob are different enough that they need fully separate implementations of the interface. --- analog_reader.go | 63 ++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/analog_reader.go b/analog_reader.go index 7b65e41..3c68ef5 100644 --- a/analog_reader.go +++ b/analog_reader.go @@ -2,6 +2,7 @@ package europi import ( "machine" + "math" ) const ( @@ -24,45 +25,40 @@ type AnalogReader interface { ReadVoltage() float32 Percent() float32 Range(steps uint16) uint16 - read() uint16 } -type analogReader struct { +type analogInput struct { machine.ADC - samples uint16 } -func newAnalogReader(pin machine.Pin) *analogReader { +func newAnalogInput(pin machine.Pin) *analogInput { adc := machine.ADC{Pin: pin} adc.Configure(machine.ADCConfig{}) - return &analogReader{ - ADC: adc, - samples: defaultSamples, - } + return &analogInput{ADC: adc, samples: defaultSamples} } // Samples sets the number of reads for an more accurate average read. -func (a *analogReader) Samples(samples uint16) { +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 *analogReader) Percent() float32 { +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 *analogReader) ReadVoltage() float32 { +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 *analogReader) Range(steps uint16) uint16 { +func (a *analogInput) Range(steps uint16) uint16 { return uint16(a.Percent() * float32(steps)) } -func (a *analogReader) read() uint16 { +func (a *analogInput) read() uint16 { var sum int for i := 0; i < int(a.samples); i++ { sum += Clamp(int(a.Get())-calibratedMinAI, 0, calibratedMaxAI) @@ -70,22 +66,41 @@ func (a *analogReader) read() uint16 { return uint16(sum / int(a.samples)) } -type analogInput struct { - AnalogReader +type knob struct { + machine.ADC + samples uint16 } -func newAnalogInput(pin machine.Pin) *analogInput { - return &analogInput{ - newAnalogReader(pin), - } +func newKnob(pin machine.Pin) *knob { + adc := machine.ADC{Pin: pin} + adc.Configure(machine.ADCConfig{}) + return &knob{ADC: adc, samples: defaultSamples} } -type knob struct { - AnalogReader +// Samples sets the number of reads for an more accurate average read. +func (k *knob) Samples(samples uint16) { + k.samples = samples } -func newKnob(pin machine.Pin) *knob { - return &knob{ - newAnalogReader(pin), +// 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)) } From aa0839e675a56118542b4fdcb94be6420942ff43 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 20 Oct 2022 19:55:24 -0500 Subject: [PATCH 5/5] Do not embed hardware types. Replace GetInstance() with New() singleton wrapper. --- analog_reader.go | 12 ++++++------ digital_reader.go | 25 +++++++++++-------------- europi.go | 11 +++++------ scripts/clockwerk/clockwerk.go | 2 +- scripts/diagnostics/diagnostics.go | 5 ++--- 5 files changed, 25 insertions(+), 30 deletions(-) diff --git a/analog_reader.go b/analog_reader.go index 3c68ef5..04636d7 100644 --- a/analog_reader.go +++ b/analog_reader.go @@ -28,14 +28,14 @@ type AnalogReader interface { } type analogInput struct { - machine.ADC + adc machine.ADC samples uint16 } func newAnalogInput(pin machine.Pin) *analogInput { adc := machine.ADC{Pin: pin} adc.Configure(machine.ADCConfig{}) - return &analogInput{ADC: adc, samples: defaultSamples} + return &analogInput{adc: adc, samples: defaultSamples} } // Samples sets the number of reads for an more accurate average read. @@ -61,20 +61,20 @@ func (a *analogInput) Range(steps uint16) uint16 { func (a *analogInput) read() uint16 { var sum int for i := 0; i < int(a.samples); i++ { - sum += Clamp(int(a.Get())-calibratedMinAI, 0, calibratedMaxAI) + sum += Clamp(int(a.adc.Get())-calibratedMinAI, 0, calibratedMaxAI) } return uint16(sum / int(a.samples)) } type knob struct { - machine.ADC + adc machine.ADC samples uint16 } func newKnob(pin machine.Pin) *knob { adc := machine.ADC{Pin: pin} adc.Configure(machine.ADCConfig{}) - return &knob{ADC: adc, samples: defaultSamples} + return &knob{adc: adc, samples: defaultSamples} } // Samples sets the number of reads for an more accurate average read. @@ -100,7 +100,7 @@ func (k *knob) Range(steps uint16) uint16 { func (k *knob) read() uint16 { var sum int for i := 0; i < int(k.samples); i++ { - sum += int(k.Get()) + sum += int(k.adc.Get()) } return uint16(sum / int(k.samples)) } diff --git a/digital_reader.go b/digital_reader.go index feb4a22..3515198 100644 --- a/digital_reader.go +++ b/digital_reader.go @@ -7,28 +7,25 @@ import ( const defaultDebounceDelay = time.Duration(50 * time.Millisecond) -type callback func(machine.Pin) - // DigitalReader is an interface for common digital inputs methods. type DigitalReader interface { - Handler(callback) - HandlerWithDebounce(callback, time.Duration) + Handler(func(machine.Pin)) + HandlerWithDebounce(func(machine.Pin), time.Duration) LastInput() time.Time Value() bool } type digitalReader struct { - machine.Pin - + pin machine.Pin debounceDelay time.Duration lastInput time.Time - callback callback + callback func(machine.Pin) } func newDigitalReader(pin machine.Pin) *digitalReader { pin.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) return &digitalReader{ - Pin: pin, + pin: pin, lastInput: time.Now(), debounceDelay: defaultDebounceDelay, } @@ -42,19 +39,19 @@ func (d *digitalReader) LastInput() time.Time { // Value returns true if the input is high (above 0.8v), else false. func (d *digitalReader) Value() bool { // Invert signal to match expected behavior. - return !d.Pin.Get() + return !d.pin.Get() } // Handler sets the callback function to be call when a rising edge is detected. -func (d *digitalReader) Handler(c callback) { - d.HandlerWithDebounce(c, 0) +func (d *digitalReader) Handler(callback func(machine.Pin)) { + d.HandlerWithDebounce(callback, 0) } // Handler sets the callback function to be call when a rising edge is detected and debounce delay time has elapsed. -func (d *digitalReader) HandlerWithDebounce(c callback, delay time.Duration) { - d.callback = c +func (d *digitalReader) HandlerWithDebounce(callback func(machine.Pin), delay time.Duration) { + d.callback = callback d.debounceDelay = delay - d.Pin.SetInterrupt(machine.PinFalling, d.debounceWrapper) + d.pin.SetInterrupt(machine.PinFalling, d.debounceWrapper) } func (d *digitalReader) debounceWrapper(p machine.Pin) { diff --git a/europi.go b/europi.go index 3cbee9a..4ac7353 100644 --- a/europi.go +++ b/europi.go @@ -72,8 +72,11 @@ type EuroPi struct { CV [6]*output } -func init() { - europi = new() +func New() *EuroPi { + if europi == nil { + europi = new() + } + return europi } func new() *EuroPi { @@ -105,7 +108,3 @@ func new() *EuroPi { CV: [6]*output{cv1, cv2, cv3, cv4, cv5, cv6}, } } - -func GetInstance() *EuroPi { - return europi -} diff --git a/scripts/clockwerk/clockwerk.go b/scripts/clockwerk/clockwerk.go index 48bbc1f..b78aea4 100644 --- a/scripts/clockwerk/clockwerk.go +++ b/scripts/clockwerk/clockwerk.go @@ -49,7 +49,7 @@ var ( // Positive values are multiplications and negative values are divisions. DefaultFactor = [6]int{1, 2, 4, -2, -4, -8} FactorChoices []int - EuroPi = europi.GetInstance() + EuroPi = europi.New() ) func init() { diff --git a/scripts/diagnostics/diagnostics.go b/scripts/diagnostics/diagnostics.go index 66d5ca0..3112cb0 100644 --- a/scripts/diagnostics/diagnostics.go +++ b/scripts/diagnostics/diagnostics.go @@ -11,6 +11,8 @@ import ( europi "github.com/awonak/EuroPiGo" ) +var EuroPi = europi.New() + type MyApp struct { knobsDisplayPercent bool displayShouldUpdate bool @@ -26,8 +28,6 @@ func (m *MyApp) updateDisplay() { } m.displayShouldUpdate = false - EuroPi := europi.GetInstance() - EuroPi.Display.ClearBuffer() // Highlight the border of the oled display. @@ -56,7 +56,6 @@ func main() { myApp := MyApp{ staticCv: 5, } - EuroPi := europi.GetInstance() // Demonstrate adding a IRQ handler to B1 and B2. EuroPi.B1.Handler(func(p machine.Pin) {