interface: add an abstraction layer for character displays

the characterdisplay package is an abstraction layer for controlling
character displays

also includes refactors to the hd44780 package to fit the
characterdisplay Controller interface
This commit is contained in:
Matthew Dale 2015-02-21 22:50:56 -08:00
parent dac729e4fd
commit 915b3b76a7
8 changed files with 489 additions and 364 deletions

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -3,12 +3,6 @@ Package hd44780 allows controlling an HD44780-compatible character LCD
controller. Currently the library is write-only and does not support controller. Currently the library is write-only and does not support
reading from the display controller. 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 Resources
This library is based three other HD44780 libraries: This library is based three other HD44780 libraries:
@ -25,14 +19,24 @@ import (
"github.com/kidoman/embd" "github.com/kidoman/embd"
) )
type Polarity bool
type entryMode byte type entryMode byte
type displayMode byte type displayMode byte
type functionMode 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 ( const (
Negative Polarity = false Negative BacklightPolarity = false
Positive Polarity = true Positive BacklightPolarity = true
writeDelay = 37 * time.Microsecond writeDelay = 37 * time.Microsecond
pulseDelay = 1 * time.Microsecond pulseDelay = 1 * time.Microsecond
@ -84,18 +88,38 @@ const (
// HD44780 represents an HD44780-compatible character LCD controller. // HD44780 represents an HD44780-compatible character LCD controller.
type HD44780 struct { type HD44780 struct {
Connection Connection
eMode entryMode eMode entryMode
dMode displayMode dMode displayMode
fMode functionMode fMode functionMode
rowAddr RowAddress
} }
// NewGPIO creates a new HD44780 connected by a 4-bit GPIO bus. // NewGPIO creates a new HD44780 connected by a 4-bit GPIO bus.
func NewGPIO( func NewGPIO(
rs, en, d4, d5, d6, d7, backlight embd.DigitalPin, rs, en, d4, d5, d6, d7, backlight interface{},
blPolarity Polarity, blPolarity BacklightPolarity,
rowAddr RowAddress,
modes ...ModeSetter, modes ...ModeSetter,
) (*HD44780, error) { ) (*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 { for _, pin := range pins {
if pin == nil { if pin == nil {
continue continue
@ -107,29 +131,45 @@ func NewGPIO(
} }
} }
return New( 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..., modes...,
) )
} }
// NewI2C creates a new HD44780 connected by an I²C bus. // NewI2C creates a new HD44780 connected by an I²C bus.
func NewI2C(i2c embd.I2CBus, addr byte, pinMap I2CPinMap, modes ...ModeSetter) (*HD44780, error) { func NewI2C(
return New(NewI2CConnection(i2c, addr, pinMap), modes...) 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. // 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{ controller := &HD44780{
Connection: bus, Connection: bus,
eMode: 0x00, eMode: 0x00,
dMode: 0x00, dMode: 0x00,
fMode: 0x00, fMode: 0x00,
rowAddr: rowAddr,
} }
err := controller.lcdInit() err := controller.lcdInit()
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = controller.SetMode(modes...) err = controller.SetMode(append(DefaultModes, modes...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -146,6 +186,18 @@ func (controller *HD44780) lcdInit() error {
return controller.WriteInstruction(lcdInit4bit) 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. // ModeSetter defines a function used for setting modes on an HD44780.
// ModeSetters must be used with the SetMode function or in a constructor. // ModeSetters must be used with the SetMode function or in a constructor.
type ModeSetter func(*HD44780) 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. // Clear clears the display and mode settings sets the cursor to the home position.
func (hd *HD44780) Clear() error { func (hd *HD44780) Clear() error {
err := hd.WriteInstruction(lcdClearDisplay) err := hd.WriteInstruction(lcdClearDisplay)
if err != nil {
return err
}
time.Sleep(clearDelay) 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. // SetCursor sets the input cursor to the given position.
func (hd *HD44780) SetCursor(value byte) error { 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) return hd.WriteInstruction(lcdSetDDRamAddr | value)
} }
@ -357,13 +426,13 @@ type GPIOConnection struct {
RS, EN embd.DigitalPin RS, EN embd.DigitalPin
D4, D5, D6, D7 embd.DigitalPin D4, D5, D6, D7 embd.DigitalPin
Backlight embd.DigitalPin Backlight embd.DigitalPin
BLPolarity Polarity BLPolarity BacklightPolarity
} }
// NewGPIOConnection returns a new Connection based on a 4-bit GPIO bus. // NewGPIOConnection returns a new Connection based on a 4-bit GPIO bus.
func NewGPIOConnection( func NewGPIOConnection(
rs, en, d4, d5, d6, d7, backlight embd.DigitalPin, rs, en, d4, d5, d6, d7, backlight embd.DigitalPin,
blPolarity Polarity, blPolarity BacklightPolarity,
) *GPIOConnection { ) *GPIOConnection {
return &GPIOConnection{ return &GPIOConnection{
RS: rs, RS: rs,
@ -480,7 +549,7 @@ type I2CPinMap struct {
RS, RW, EN byte RS, RW, EN byte
D4, D5, D6, D7 byte D4, D5, D6, D7 byte
Backlight byte Backlight byte
BLPolarity Polarity BLPolarity BacklightPolarity
} }
var ( var (

View File

@ -9,7 +9,13 @@ import (
"github.com/kidoman/embd" "github.com/kidoman/embd"
) )
const testAddr byte = 0x20 const (
testAddr byte = 0x20
cols = 20
rows = 4
)
var testRowAddr RowAddress = RowAddress20Col
type mockDigitalPin struct { type mockDigitalPin struct {
direction embd.Direction direction embd.Direction
@ -60,6 +66,11 @@ type instruction struct {
data byte 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 { func (ins *instruction) printAsBinary() string {
return fmt.Sprintf("RS:%d|Byte:%s", ins.rs, printByteAsBinary(ins.data)) return fmt.Sprintf("RS:%d|Byte:%s", ins.rs, printByteAsBinary(ins.data))
} }
@ -73,7 +84,7 @@ func printInstructionsAsBinary(ins []instruction) string {
} }
func newMockGPIOConnection() *mockGPIOConnection { func newMockGPIOConnection() *mockGPIOConnection {
be := &mockGPIOConnection{ conn := &mockGPIOConnection{
rs: newMockDigitalPin(), rs: newMockDigitalPin(),
en: newMockDigitalPin(), en: newMockDigitalPin(),
d4: newMockDigitalPin(), d4: newMockDigitalPin(),
@ -87,32 +98,32 @@ func newMockGPIOConnection() *mockGPIOConnection {
var b byte = 0x00 var b byte = 0x00
var rs int = 0 var rs int = 0
// wait for EN low,high,low then read high nibble // wait for EN low,high,low then read high nibble
if <-be.en.values == embd.Low && if <-conn.en.values == embd.Low &&
<-be.en.values == embd.High && <-conn.en.values == embd.High &&
<-be.en.values == embd.Low { <-conn.en.values == embd.Low {
rs = <-be.rs.values rs = <-conn.rs.values
b |= byte(<-be.d4.values) << 4 b |= byte(<-conn.d4.values) << 4
b |= byte(<-be.d5.values) << 5 b |= byte(<-conn.d5.values) << 5
b |= byte(<-be.d6.values) << 6 b |= byte(<-conn.d6.values) << 6
b |= byte(<-be.d7.values) << 7 b |= byte(<-conn.d7.values) << 7
} }
// wait for EN low,high,low then read low nibble // wait for EN low,high,low then read low nibble
if <-be.en.values == embd.Low && if <-conn.en.values == embd.Low &&
<-be.en.values == embd.High && <-conn.en.values == embd.High &&
<-be.en.values == embd.Low { <-conn.en.values == embd.Low {
b |= byte(<-be.d4.values) b |= byte(<-conn.d4.values)
b |= byte(<-be.d5.values) << 1 b |= byte(<-conn.d5.values) << 1
b |= byte(<-be.d6.values) << 2 b |= byte(<-conn.d6.values) << 2
b |= byte(<-be.d7.values) << 3 b |= byte(<-conn.d7.values) << 3
be.writes = append(be.writes, instruction{rs, b}) conn.writes = append(conn.writes, instruction{rs, b})
} }
} }
}() }()
return be return conn
} }
func (be *mockGPIOConnection) pins() []*mockDigitalPin { func (conn *mockGPIOConnection) pins() []*mockDigitalPin {
return []*mockDigitalPin{be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight} return []*mockDigitalPin{conn.rs, conn.en, conn.d4, conn.d5, conn.d6, conn.d7, conn.backlight}
} }
type mockI2CBus struct { type mockI2CBus struct {
@ -156,9 +167,9 @@ func printBytesAsBinary(bytes []byte) string {
} }
func TestInitialize4Bit_directionOut(t *testing.T) { func TestInitialize4Bit_directionOut(t *testing.T) {
be := newMockGPIOConnection() mock := newMockGPIOConnection()
NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr)
for idx, pin := range be.pins() { for idx, pin := range mock.pins() {
if pin.direction != embd.Out { if pin.direction != embd.Out {
t.Errorf("Pin %d not set to direction Out", idx) 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) { func TestInitialize4Bit_lcdInit(t *testing.T) {
be := newMockGPIOConnection() mock := newMockGPIOConnection()
NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) gpio, _ := NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr)
instructions := []instruction{ instructions := []instruction{
instruction{embd.Low, lcdInit}, instruction{embd.Low, lcdInit},
instruction{embd.Low, lcdInit4bit}, instruction{embd.Low, lcdInit4bit},
instruction{embd.Low, byte(lcdSetEntryMode)}, instruction{embd.Low, byte(gpio.eMode | lcdSetEntryMode)},
instruction{embd.Low, byte(lcdSetDisplayMode)}, instruction{embd.Low, byte(gpio.dMode | lcdSetDisplayMode)},
instruction{embd.Low, byte(lcdSetFunctionMode)}, instruction{embd.Low, byte(gpio.fMode | lcdSetFunctionMode)},
} }
if !reflect.DeepEqual(instructions, be.writes) { if !reflect.DeepEqual(instructions, mock.writes) {
t.Errorf( t.Errorf(
"\nExpected\t%s\nActual\t\t%+v", "\nExpected\t%s\nActual\t\t%+v",
printInstructionsAsBinary(instructions), printInstructionsAsBinary(instructions),
printInstructionsAsBinary(be.writes)) printInstructionsAsBinary(mock.writes))
} }
} }
func TestGPIOConnectionClose(t *testing.T) { func TestGPIOConnectionClose(t *testing.T) {
be := newMockGPIOConnection() mock := newMockGPIOConnection()
bus, _ := NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) bus, _ := NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr)
bus.Close() bus.Close()
for idx, pin := range be.pins() { for idx, pin := range mock.pins() {
if !pin.closed { if !pin.closed {
t.Errorf("Pin %d was not closed", idx) t.Errorf("Pin %d was not closed", idx)
} }
@ -253,3 +264,55 @@ func TestI2CConnectionClose(t *testing.T) {
t.Error("I2C bus was not closed") 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")
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -22,22 +22,32 @@ func main() {
bus := embd.NewI2CBus(1) bus := embd.NewI2CBus(1)
display, err := hd44780.NewI2CCharacterDisplay( hd, err := hd44780.NewI2C(
bus, bus,
0x20, 0x20,
hd44780.PCF8574PinMap, hd44780.PCF8574PinMap,
20, hd44780.RowAddress20Col,
4,
hd44780.TwoLine, hd44780.TwoLine,
hd44780.BlinkOn, hd44780.BlinkOn,
) )
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer display.Close() defer hd.Close()
display.Clear() hd.Clear()
display.Message("Hello, world!\n@embd") 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) time.Sleep(10 * time.Second)
display.BacklightOff() hd.BacklightOff()
} }