diff --git a/controller/hd44780/character_display.go b/controller/hd44780/character_display.go deleted file mode 100644 index 6c23099..0000000 --- a/controller/hd44780/character_display.go +++ /dev/null @@ -1,212 +0,0 @@ -package hd44780 - -import ( - "github.com/golang/glog" - "github.com/kidoman/embd" -) - -// DefaultModes are the default initialization modes for a CharacterDisplay. -var DefaultModes []ModeSetter = []ModeSetter{ - FourBitMode, - OneLine, - Dots5x8, - EntryIncrement, - EntryShiftOff, - DisplayOn, - CursorOff, - BlinkOff, -} - -// CharacterDisplay represents an abstract character display and provides a -// convenience layer on top of the basic HD44780 library. -type CharacterDisplay struct { - *HD44780 - Cols int - Rows int - p *position -} - -type position struct { - col int - row int -} - -// NewGPIOCharacterDisplay creates a new CharacterDisplay connected by a 4-bit GPIO bus. -func NewGPIOCharacterDisplay( - rs, en, d4, d5, d6, d7, backlight interface{}, - blPolarity Polarity, - cols, rows int, - modes ...ModeSetter, -) (*CharacterDisplay, error) { - pinKeys := []interface{}{rs, en, d4, d5, d6, d7, backlight} - pins := [7]embd.DigitalPin{} - for idx, key := range pinKeys { - if key == nil { - continue - } - var digitalPin embd.DigitalPin - if pin, ok := key.(embd.DigitalPin); ok { - digitalPin = pin - } else { - var err error - digitalPin, err = embd.NewDigitalPin(key) - if err != nil { - glog.V(1).Infof("hd44780: error creating digital pin %+v: %s", key, err) - return nil, err - } - } - pins[idx] = digitalPin - } - hd, err := NewGPIO( - pins[0], - pins[1], - pins[2], - pins[3], - pins[4], - pins[5], - pins[6], - blPolarity, - append(DefaultModes, modes...)..., - ) - if err != nil { - return nil, err - } - return NewCharacterDisplay(hd, cols, rows) -} - -// NewI2CCharacterDisplay creates a new CharacterDisplay connected by an I²C bus. -func NewI2CCharacterDisplay( - i2c embd.I2CBus, - addr byte, - pinMap I2CPinMap, - cols, rows int, - modes ...ModeSetter, -) (*CharacterDisplay, error) { - hd, err := NewI2C(i2c, addr, pinMap, append(DefaultModes, modes...)...) - if err != nil { - return nil, err - } - return NewCharacterDisplay(hd, cols, rows) -} - -// NewCharacterDisplay creates a new character display abstraction for an -// HD44780-compatible controller. -func NewCharacterDisplay(hd *HD44780, cols, rows int) (*CharacterDisplay, error) { - display := &CharacterDisplay{ - HD44780: hd, - Cols: cols, - Rows: rows, - p: &position{0, 0}, - } - err := display.BacklightOn() - if err != nil { - return nil, err - } - return display, nil -} - -// Home moves the cursor and all characters to the home position. -func (disp *CharacterDisplay) Home() error { - disp.currentPosition(0, 0) - return disp.HD44780.Home() -} - -// Clear clears the display, preserving the mode settings and setting the correct home. -func (disp *CharacterDisplay) Clear() error { - disp.currentPosition(0, 0) - err := disp.HD44780.Clear() - if err != nil { - return err - } - err = disp.SetMode() - if err != nil { - return err - } - if !disp.isLeftToRight() { - return disp.SetCursor(disp.Cols-1, 0) - } - return nil -} - -// Message prints the given string on the display. -func (disp *CharacterDisplay) Message(message string) error { - bytes := []byte(message) - for _, b := range bytes { - if b == byte('\n') { - err := disp.Newline() - if err != nil { - return err - } - continue - } - err := disp.WriteChar(b) - if err != nil { - return err - } - if disp.isLeftToRight() { - disp.p.col++ - } else { - disp.p.col-- - } - if disp.p.col >= disp.Cols || disp.p.col < 0 { - err := disp.Newline() - if err != nil { - return err - } - } - } - return nil -} - -// Newline moves the input cursor to the beginning of the next line. -func (disp *CharacterDisplay) Newline() error { - var col int - if disp.isLeftToRight() { - col = 0 - } else { - col = disp.Cols - 1 - } - return disp.SetCursor(col, disp.p.row+1) -} - -func (disp *CharacterDisplay) isLeftToRight() bool { - // EntryIncrement and EntryShiftOn is right-to-left - // EntryDecrement and EntryShiftOn is left-to-right - // EntryIncrement and EntryShiftOff is left-to-right - // EntryDecrement and EntryShiftOff is right-to-left - return disp.EntryIncrementEnabled() != disp.EntryShiftEnabled() -} - -// SetCursor sets the input cursor to the given position. -func (disp *CharacterDisplay) SetCursor(col, row int) error { - if row >= disp.Rows { - row = disp.Rows - 1 - } - disp.currentPosition(col, row) - return disp.HD44780.SetCursor(byte(col) + disp.lcdRowOffset(row)) -} - -func (disp *CharacterDisplay) lcdRowOffset(row int) byte { - // Offset for up to 4 rows - if row > 3 { - row = 3 - } - switch disp.Cols { - case 16: - // 16-char line mappings - return []byte{0x00, 0x40, 0x10, 0x50}[row] - default: - // default to the 20-char line mappings - return []byte{0x00, 0x40, 0x14, 0x54}[row] - } -} - -func (disp *CharacterDisplay) currentPosition(col, row int) { - disp.p.col = col - disp.p.row = row -} - -// Close closes the underlying HD44780 controller. -func (disp *CharacterDisplay) Close() error { - return disp.HD44780.Close() -} diff --git a/controller/hd44780/character_display_test.go b/controller/hd44780/character_display_test.go deleted file mode 100644 index e2eca42..0000000 --- a/controller/hd44780/character_display_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package hd44780 - -import ( - "testing" - - "github.com/kidoman/embd" -) - -const ( - rows = 20 - cols = 4 -) - -func TestNewGPIOCharacterDisplay_initPins(t *testing.T) { - var pins []*mockDigitalPin - for i := 0; i < 7; i++ { - pins = append(pins, newMockDigitalPin()) - } - NewGPIOCharacterDisplay( - pins[0], - pins[1], - pins[2], - pins[3], - pins[4], - pins[5], - pins[6], - Negative, - cols, - rows, - ) - for idx, pin := range pins { - if pin.direction != embd.Out { - t.Errorf("Pin %d not set to direction Out(%d), set to %d", idx, embd.Out, pin.direction) - } - } -} - -func TestDefaultModes(t *testing.T) { - displayGPIO, _ := NewGPIOCharacterDisplay( - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - Negative, - cols, - rows, - ) - displayI2C, _ := NewI2CCharacterDisplay( - newMockI2CBus(), - testAddr, - MJKDZPinMap, - cols, - rows, - ) - - for idx, display := range []*CharacterDisplay{displayGPIO, displayI2C} { - if display.EightBitModeEnabled() { - t.Errorf("Display %d: Expected display to be initialized in 4-bit mode", idx) - } - if display.TwoLineEnabled() { - t.Errorf("Display %d: Expected display to be initialized in one-line mode", idx) - } - if display.Dots5x10Enabled() { - t.Errorf("Display %d: Expected display to be initialized in 5x8-dots mode", idx) - } - if !display.EntryIncrementEnabled() { - t.Errorf("Display %d: Expected display to be initialized in entry increment mode", idx) - } - if display.EntryShiftEnabled() { - t.Errorf("Display %d: Expected display to be initialized in entry shift off mode", idx) - } - if !display.DisplayEnabled() { - t.Errorf("Display %d: Expected display to be initialized in display on mode", idx) - } - if display.CursorEnabled() { - t.Errorf("Display %d: Expected display to be initialized in cursor off mode", idx) - } - if display.BlinkEnabled() { - t.Errorf("Display %d: Expected display to be initialized in blink off mode", idx) - } - } -} diff --git a/controller/hd44780/hd44780.go b/controller/hd44780/hd44780.go index a977054..3531230 100644 --- a/controller/hd44780/hd44780.go +++ b/controller/hd44780/hd44780.go @@ -3,12 +3,6 @@ Package hd44780 allows controlling an HD44780-compatible character LCD controller. Currently the library is write-only and does not support reading from the display controller. -Character Display - -The CharacterDisplay type provides a convenience layer on top of the basic -HD44780 library. It includes functions for easier message printing and abstracts -some of the quirky behaviors of 4-row displays. - Resources This library is based three other HD44780 libraries: @@ -25,14 +19,24 @@ import ( "github.com/kidoman/embd" ) -type Polarity bool type entryMode byte type displayMode byte type functionMode byte +// RowAddress defines the DDRAM address of the first column of each row, up to 4 rows. +type RowAddress [4]byte + +var ( + RowAddress16Col RowAddress = [4]byte{0x00, 0x40, 0x10, 0x50} // row addresses for 16-column displays + RowAddress20Col RowAddress = [4]byte{0x00, 0x40, 0x14, 0x54} // row addresses for 20-column displays +) + +// BacklightPolarity defines the polarity of the backlight switch, either positive or negative +type BacklightPolarity bool + const ( - Negative Polarity = false - Positive Polarity = true + Negative BacklightPolarity = false + Positive BacklightPolarity = true writeDelay = 37 * time.Microsecond pulseDelay = 1 * time.Microsecond @@ -84,18 +88,38 @@ const ( // HD44780 represents an HD44780-compatible character LCD controller. type HD44780 struct { Connection - eMode entryMode - dMode displayMode - fMode functionMode + eMode entryMode + dMode displayMode + fMode functionMode + rowAddr RowAddress } // NewGPIO creates a new HD44780 connected by a 4-bit GPIO bus. func NewGPIO( - rs, en, d4, d5, d6, d7, backlight embd.DigitalPin, - blPolarity Polarity, + rs, en, d4, d5, d6, d7, backlight interface{}, + blPolarity BacklightPolarity, + rowAddr RowAddress, modes ...ModeSetter, ) (*HD44780, error) { - pins := []embd.DigitalPin{rs, en, d4, d5, d6, d7, backlight} + pinKeys := []interface{}{rs, en, d4, d5, d6, d7, backlight} + pins := [7]embd.DigitalPin{} + for idx, key := range pinKeys { + if key == nil { + continue + } + var digitalPin embd.DigitalPin + if pin, ok := key.(embd.DigitalPin); ok { + digitalPin = pin + } else { + var err error + digitalPin, err = embd.NewDigitalPin(key) + if err != nil { + glog.V(1).Infof("hd44780: error creating digital pin %+v: %s", key, err) + return nil, err + } + } + pins[idx] = digitalPin + } for _, pin := range pins { if pin == nil { continue @@ -107,29 +131,45 @@ func NewGPIO( } } return New( - NewGPIOConnection(rs, en, d4, d5, d6, d7, backlight, blPolarity), + NewGPIOConnection( + pins[0], + pins[1], + pins[2], + pins[3], + pins[4], + pins[5], + pins[6], + blPolarity), + rowAddr, modes..., ) } // NewI2C creates a new HD44780 connected by an I²C bus. -func NewI2C(i2c embd.I2CBus, addr byte, pinMap I2CPinMap, modes ...ModeSetter) (*HD44780, error) { - return New(NewI2CConnection(i2c, addr, pinMap), modes...) +func NewI2C( + i2c embd.I2CBus, + addr byte, + pinMap I2CPinMap, + rowAddr RowAddress, + modes ...ModeSetter, +) (*HD44780, error) { + return New(NewI2CConnection(i2c, addr, pinMap), rowAddr, modes...) } // New creates a new HD44780 connected by a Connection bus. -func New(bus Connection, modes ...ModeSetter) (*HD44780, error) { +func New(bus Connection, rowAddr RowAddress, modes ...ModeSetter) (*HD44780, error) { controller := &HD44780{ Connection: bus, eMode: 0x00, dMode: 0x00, fMode: 0x00, + rowAddr: rowAddr, } err := controller.lcdInit() if err != nil { return nil, err } - err = controller.SetMode(modes...) + err = controller.SetMode(append(DefaultModes, modes...)...) if err != nil { return nil, err } @@ -146,6 +186,18 @@ func (controller *HD44780) lcdInit() error { return controller.WriteInstruction(lcdInit4bit) } +// DefaultModes are the default initialization modes for an HD44780. +var DefaultModes []ModeSetter = []ModeSetter{ + FourBitMode, + OneLine, + Dots5x8, + EntryIncrement, + EntryShiftOff, + DisplayOn, + CursorOff, + BlinkOff, +} + // ModeSetter defines a function used for setting modes on an HD44780. // ModeSetters must be used with the SetMode function or in a constructor. type ModeSetter func(*HD44780) @@ -312,12 +364,29 @@ func (hd *HD44780) Home() error { // Clear clears the display and mode settings sets the cursor to the home position. func (hd *HD44780) Clear() error { err := hd.WriteInstruction(lcdClearDisplay) + if err != nil { + return err + } time.Sleep(clearDelay) - return err + // have to set mode here because clear also clears some mode settings + return hd.SetMode() } -// SetCursor sets the input cursor to the given bye. -func (hd *HD44780) SetCursor(value byte) error { +// SetCursor sets the input cursor to the given position. +func (hd *HD44780) SetCursor(col, row int) error { + return hd.SetDDRamAddr(byte(col) + hd.lcdRowOffset(row)) +} + +func (hd *HD44780) lcdRowOffset(row int) byte { + // Offset for up to 4 rows + if row > 3 { + row = 3 + } + return hd.rowAddr[row] +} + +// SetDDRamAddr sets the input cursor to the given address. +func (hd *HD44780) SetDDRamAddr(value byte) error { return hd.WriteInstruction(lcdSetDDRamAddr | value) } @@ -357,13 +426,13 @@ type GPIOConnection struct { RS, EN embd.DigitalPin D4, D5, D6, D7 embd.DigitalPin Backlight embd.DigitalPin - BLPolarity Polarity + BLPolarity BacklightPolarity } // NewGPIOConnection returns a new Connection based on a 4-bit GPIO bus. func NewGPIOConnection( rs, en, d4, d5, d6, d7, backlight embd.DigitalPin, - blPolarity Polarity, + blPolarity BacklightPolarity, ) *GPIOConnection { return &GPIOConnection{ RS: rs, @@ -480,7 +549,7 @@ type I2CPinMap struct { RS, RW, EN byte D4, D5, D6, D7 byte Backlight byte - BLPolarity Polarity + BLPolarity BacklightPolarity } var ( diff --git a/controller/hd44780/hd44780_test.go b/controller/hd44780/hd44780_test.go index a061140..b05dddc 100644 --- a/controller/hd44780/hd44780_test.go +++ b/controller/hd44780/hd44780_test.go @@ -9,7 +9,13 @@ import ( "github.com/kidoman/embd" ) -const testAddr byte = 0x20 +const ( + testAddr byte = 0x20 + cols = 20 + rows = 4 +) + +var testRowAddr RowAddress = RowAddress20Col type mockDigitalPin struct { direction embd.Direction @@ -60,6 +66,11 @@ type instruction struct { data byte } +func (conn *mockGPIOConnection) Write(rs bool, data byte) error { return nil } +func (conn *mockGPIOConnection) BacklightOff() error { return nil } +func (conn *mockGPIOConnection) BacklightOn() error { return nil } +func (conn *mockGPIOConnection) Close() error { return nil } + func (ins *instruction) printAsBinary() string { return fmt.Sprintf("RS:%d|Byte:%s", ins.rs, printByteAsBinary(ins.data)) } @@ -73,7 +84,7 @@ func printInstructionsAsBinary(ins []instruction) string { } func newMockGPIOConnection() *mockGPIOConnection { - be := &mockGPIOConnection{ + conn := &mockGPIOConnection{ rs: newMockDigitalPin(), en: newMockDigitalPin(), d4: newMockDigitalPin(), @@ -87,32 +98,32 @@ func newMockGPIOConnection() *mockGPIOConnection { var b byte = 0x00 var rs int = 0 // wait for EN low,high,low then read high nibble - if <-be.en.values == embd.Low && - <-be.en.values == embd.High && - <-be.en.values == embd.Low { - rs = <-be.rs.values - b |= byte(<-be.d4.values) << 4 - b |= byte(<-be.d5.values) << 5 - b |= byte(<-be.d6.values) << 6 - b |= byte(<-be.d7.values) << 7 + if <-conn.en.values == embd.Low && + <-conn.en.values == embd.High && + <-conn.en.values == embd.Low { + rs = <-conn.rs.values + b |= byte(<-conn.d4.values) << 4 + b |= byte(<-conn.d5.values) << 5 + b |= byte(<-conn.d6.values) << 6 + b |= byte(<-conn.d7.values) << 7 } // wait for EN low,high,low then read low nibble - if <-be.en.values == embd.Low && - <-be.en.values == embd.High && - <-be.en.values == embd.Low { - b |= byte(<-be.d4.values) - b |= byte(<-be.d5.values) << 1 - b |= byte(<-be.d6.values) << 2 - b |= byte(<-be.d7.values) << 3 - be.writes = append(be.writes, instruction{rs, b}) + if <-conn.en.values == embd.Low && + <-conn.en.values == embd.High && + <-conn.en.values == embd.Low { + b |= byte(<-conn.d4.values) + b |= byte(<-conn.d5.values) << 1 + b |= byte(<-conn.d6.values) << 2 + b |= byte(<-conn.d7.values) << 3 + conn.writes = append(conn.writes, instruction{rs, b}) } } }() - return be + return conn } -func (be *mockGPIOConnection) pins() []*mockDigitalPin { - return []*mockDigitalPin{be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight} +func (conn *mockGPIOConnection) pins() []*mockDigitalPin { + return []*mockDigitalPin{conn.rs, conn.en, conn.d4, conn.d5, conn.d6, conn.d7, conn.backlight} } type mockI2CBus struct { @@ -156,9 +167,9 @@ func printBytesAsBinary(bytes []byte) string { } func TestInitialize4Bit_directionOut(t *testing.T) { - be := newMockGPIOConnection() - NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) - for idx, pin := range be.pins() { + mock := newMockGPIOConnection() + NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr) + for idx, pin := range mock.pins() { if pin.direction != embd.Out { t.Errorf("Pin %d not set to direction Out", idx) } @@ -166,29 +177,29 @@ func TestInitialize4Bit_directionOut(t *testing.T) { } func TestInitialize4Bit_lcdInit(t *testing.T) { - be := newMockGPIOConnection() - NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) + mock := newMockGPIOConnection() + gpio, _ := NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr) instructions := []instruction{ instruction{embd.Low, lcdInit}, instruction{embd.Low, lcdInit4bit}, - instruction{embd.Low, byte(lcdSetEntryMode)}, - instruction{embd.Low, byte(lcdSetDisplayMode)}, - instruction{embd.Low, byte(lcdSetFunctionMode)}, + instruction{embd.Low, byte(gpio.eMode | lcdSetEntryMode)}, + instruction{embd.Low, byte(gpio.dMode | lcdSetDisplayMode)}, + instruction{embd.Low, byte(gpio.fMode | lcdSetFunctionMode)}, } - if !reflect.DeepEqual(instructions, be.writes) { + if !reflect.DeepEqual(instructions, mock.writes) { t.Errorf( "\nExpected\t%s\nActual\t\t%+v", printInstructionsAsBinary(instructions), - printInstructionsAsBinary(be.writes)) + printInstructionsAsBinary(mock.writes)) } } func TestGPIOConnectionClose(t *testing.T) { - be := newMockGPIOConnection() - bus, _ := NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) + mock := newMockGPIOConnection() + bus, _ := NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr) bus.Close() - for idx, pin := range be.pins() { + for idx, pin := range mock.pins() { if !pin.closed { t.Errorf("Pin %d was not closed", idx) } @@ -253,3 +264,55 @@ func TestI2CConnectionClose(t *testing.T) { t.Error("I2C bus was not closed") } } + +func TestNewGPIO_initPins(t *testing.T) { + var pins []*mockDigitalPin + for i := 0; i < 7; i++ { + pins = append(pins, newMockDigitalPin()) + } + NewGPIO( + pins[0], + pins[1], + pins[2], + pins[3], + pins[4], + pins[5], + pins[6], + Negative, + testRowAddr, + ) + for idx, pin := range pins { + if pin.direction != embd.Out { + t.Errorf("Pin %d not set to direction Out(%d), set to %d", idx, embd.Out, pin.direction) + } + } +} + +func TestDefaultModes(t *testing.T) { + display, _ := New(newMockGPIOConnection(), testRowAddr) + + if display.EightBitModeEnabled() { + t.Error("Expected display to be initialized in 4-bit mode") + } + if display.TwoLineEnabled() { + t.Error("Expected display to be initialized in one-line mode") + } + if display.Dots5x10Enabled() { + t.Error("Expected display to be initialized in 5x8-dots mode") + } + if !display.EntryIncrementEnabled() { + t.Error("Expected display to be initialized in entry increment mode") + } + if display.EntryShiftEnabled() { + t.Error("Expected display to be initialized in entry shift off mode") + } + if !display.DisplayEnabled() { + t.Error("Expected display to be initialized in display on mode") + } + if display.CursorEnabled() { + t.Error("Expected display to be initialized in cursor off mode") + } + if display.BlinkEnabled() { + t.Error("Expected display to be initialized in blink off mode") + } +} diff --git a/interface/display/characterdisplay/characterdisplay.go b/interface/display/characterdisplay/characterdisplay.go new file mode 100644 index 0000000..ca866d9 --- /dev/null +++ b/interface/display/characterdisplay/characterdisplay.go @@ -0,0 +1,106 @@ +/* +Package characterdisplay provides an ease-of-use layer on top of a character +display controller. +*/ +package characterdisplay + +// Controller is an interface that describes the basic functionality of a character +// display controller. +type Controller interface { + DisplayOff() error // turns the display off + DisplayOn() error // turns the display on + CursorOff() error // sets the cursor visibility to off + CursorOn() error // sets the cursor visibility to on + BlinkOff() error // sets the cursor blink off + BlinkOn() error // sets the cursor blink on + ShiftLeft() error // moves the cursor and text one column to the left + ShiftRight() error // moves the cursor and text one column to the right + BacklightOff() error // turns the display backlight off + BacklightOn() error // turns the display backlight on + Home() error // moves the cursor to the home position + Clear() error // clears the display and moves the cursor to the home position + WriteChar(byte) error // writes a character to the display + SetCursor(col, row int) error // sets the cursor position + Close() error // closes the controller resources +} + +// Display represents an abstract character display and provides a +// ease-of-use layer on top of a character display controller. +type Display struct { + Controller + cols, rows int + p *position +} + +type position struct { + col int + row int +} + +// New creates a new Display +func New(controller Controller, cols, rows int) *Display { + return &Display{ + Controller: controller, + cols: cols, + rows: rows, + p: &position{0, 0}, + } +} + +// Home moves the cursor and all characters to the home position. +func (disp *Display) Home() error { + disp.setCurrentPosition(0, 0) + return disp.Controller.Home() +} + +// Clear clears the display, preserving the mode settings and setting the correct home. +func (disp *Display) Clear() error { + disp.setCurrentPosition(0, 0) + return disp.Controller.Clear() +} + +// Message prints the given string on the display, including interpreting newline +// characters and wrapping at the end of lines. +func (disp *Display) Message(message string) error { + bytes := []byte(message) + for _, b := range bytes { + if b == byte('\n') { + err := disp.Newline() + if err != nil { + return err + } + continue + } + err := disp.WriteChar(b) + if err != nil { + return err + } + disp.p.col++ + if disp.p.col >= disp.cols || disp.p.col < 0 { + err := disp.Newline() + if err != nil { + return err + } + } + } + return nil +} + +// Newline moves the input cursor to the beginning of the next line. +func (disp *Display) Newline() error { + return disp.SetCursor(0, disp.p.row+1) +} + +// SetCursor sets the input cursor to the given position. +func (disp *Display) SetCursor(col, row int) error { + if row >= disp.rows { + row = disp.rows - 1 + } + disp.setCurrentPosition(col, row) + return disp.Controller.SetCursor(col, row) +} + +func (disp *Display) setCurrentPosition(col, row int) { + disp.p.col = col + disp.p.row = row +} diff --git a/interface/display/characterdisplay/characterdisplay_test.go b/interface/display/characterdisplay/characterdisplay_test.go new file mode 100644 index 0000000..4bd6c39 --- /dev/null +++ b/interface/display/characterdisplay/characterdisplay_test.go @@ -0,0 +1,129 @@ +package characterdisplay + +import ( + "reflect" + "testing" + "time" +) + +const ( + rows = 4 + cols = 20 +) + +type mockController struct { + calls chan call +} + +type call struct { + name string + arguments []interface{} +} + +func noArgCall(name string) call { + return call{name, []interface{}{}} +} + +func (mock *mockController) DisplayOff() error { mock.calls <- noArgCall("DisplayOff"); return nil } +func (mock *mockController) DisplayOn() error { mock.calls <- noArgCall("DisplayOn"); return nil } +func (mock *mockController) CursorOff() error { mock.calls <- noArgCall("CursorOff"); return nil } +func (mock *mockController) CursorOn() error { mock.calls <- noArgCall("CursorOn"); return nil } +func (mock *mockController) BlinkOff() error { mock.calls <- noArgCall("BlinkOff"); return nil } +func (mock *mockController) BlinkOn() error { mock.calls <- noArgCall("BlinkOn"); return nil } +func (mock *mockController) ShiftLeft() error { mock.calls <- noArgCall("ShiftLeft"); return nil } +func (mock *mockController) ShiftRight() error { mock.calls <- noArgCall("ShiftRight"); return nil } +func (mock *mockController) BacklightOff() error { mock.calls <- noArgCall("BacklightOff"); return nil } +func (mock *mockController) BacklightOn() error { mock.calls <- noArgCall("BacklightOn"); return nil } +func (mock *mockController) Home() error { mock.calls <- noArgCall("Home"); return nil } +func (mock *mockController) Clear() error { mock.calls <- noArgCall("Clear"); return nil } +func (mock *mockController) Close() error { mock.calls <- noArgCall("Close"); return nil } +func (mock *mockController) WriteChar(b byte) error { + mock.calls <- call{"WriteChar", []interface{}{b}} + return nil +} +func (mock *mockController) SetCursor(col, row int) error { + mock.calls <- call{"SetCursor", []interface{}{col, row}} + return nil +} + +func (mock *mockController) testExpectedCalls(expectedCalls []call, t *testing.T) { + for _, expectedCall := range expectedCalls { + select { + case actualCall := <-mock.calls: + if !reflect.DeepEqual(expectedCall, actualCall) { + t.Errorf("Expected call %+v, actual call %+v", expectedCall, actualCall) + } + case <-time.After(time.Millisecond * 1): + t.Errorf("Timeout reading next call. Expected call %+v", expectedCall) + } + } + +ExtraCallsCheck: + for { + select { + case extraCall := <-mock.calls: + t.Errorf("Unexpected call %+v", extraCall) + case <-time.After(time.Millisecond * 1): + break ExtraCallsCheck + } + } +} + +func newMockController() *mockController { + return &mockController{make(chan call, 256)} +} + +func TestNewline(t *testing.T) { + mock := newMockController() + disp := New(mock, cols, rows) + disp.Newline() + + expectedCalls := []call{ + call{"SetCursor", []interface{}{0, 1}}, + } + + mock.testExpectedCalls(expectedCalls, t) +} + +func TestMessage(t *testing.T) { + mock := newMockController() + disp := New(mock, cols, rows) + disp.Message("ab") + + expectedCalls := []call{ + call{"WriteChar", []interface{}{byte('a')}}, + call{"WriteChar", []interface{}{byte('b')}}, + } + + mock.testExpectedCalls(expectedCalls, t) +} + +func TestMessage_newLine(t *testing.T) { + mock := newMockController() + disp := New(mock, cols, rows) + disp.Message("a\nb") + + expectedCalls := []call{ + call{"WriteChar", []interface{}{byte('a')}}, + call{"SetCursor", []interface{}{0, 1}}, + call{"WriteChar", []interface{}{byte('b')}}, + } + + mock.testExpectedCalls(expectedCalls, t) +} + +func TestMessage_wrap(t *testing.T) { + mock := newMockController() + disp := New(mock, cols, rows) + disp.SetCursor(cols-1, 0) + disp.Message("ab") + + expectedCalls := []call{ + call{"SetCursor", []interface{}{cols - 1, 0}}, + call{"WriteChar", []interface{}{byte('a')}}, + call{"SetCursor", []interface{}{0, 1}}, + call{"WriteChar", []interface{}{byte('b')}}, + } + + mock.testExpectedCalls(expectedCalls, t) +} diff --git a/samples/characterdisplay.go b/samples/characterdisplay.go new file mode 100644 index 0000000..7a054aa --- /dev/null +++ b/samples/characterdisplay.go @@ -0,0 +1,45 @@ +// +build ignore + +package main + +import ( + "flag" + "time" + + "github.com/kidoman/embd" + "github.com/kidoman/embd/controller/hd44780" + "github.com/kidoman/embd/interface/display/characterdisplay" + + _ "github.com/kidoman/embd/host/all" +) + +func main() { + flag.Parse() + + if err := embd.InitI2C(); err != nil { + panic(err) + } + defer embd.CloseI2C() + + bus := embd.NewI2CBus(1) + + controller, err := hd44780.NewI2C( + bus, + 0x20, + hd44780.PCF8574PinMap, + hd44780.RowAddress20Col, + hd44780.TwoLine, + hd44780.BlinkOn, + ) + if err != nil { + panic(err) + } + + display := characterdisplay.New(controller, 20, 4) + defer display.Close() + + display.Clear() + display.Message("Hello, world!\n@embd | characterdisplay") + time.Sleep(10 * time.Second) + display.BacklightOff() +} diff --git a/samples/hd44780.go b/samples/hd44780.go index 3307a4c..418fa7e 100644 --- a/samples/hd44780.go +++ b/samples/hd44780.go @@ -22,22 +22,32 @@ func main() { bus := embd.NewI2CBus(1) - display, err := hd44780.NewI2CCharacterDisplay( + hd, err := hd44780.NewI2C( bus, 0x20, hd44780.PCF8574PinMap, - 20, - 4, + hd44780.RowAddress20Col, hd44780.TwoLine, hd44780.BlinkOn, ) if err != nil { panic(err) } - defer display.Close() + defer hd.Close() - display.Clear() - display.Message("Hello, world!\n@embd") + hd.Clear() + message := "Hello, world!" + bytes := []byte(message) + for _, b := range bytes { + hd.WriteChar(b) + } + hd.SetCursor(0, 1) + + message = "@embd | hd44780" + bytes = []byte(message) + for _, b := range bytes { + hd.WriteChar(b) + } time.Sleep(10 * time.Second) - display.BacklightOff() + hd.BacklightOff() }