From cad22759dd163235d6c4609ce4c4e48316adc548 Mon Sep 17 00:00:00 2001 From: gsexton Date: Sun, 9 Feb 2025 15:19:03 -0700 Subject: [PATCH] Add Support for SparkFun SerLCD Display (#88) * Initial Creation * Bump conn version * add more documentation for 32 byte limit. minor code restructure to be more transparent --- go.mod | 2 +- go.sum | 4 +- serlcd/example_test.go | 50 ++++++ serlcd/serlcd.go | 254 +++++++++++++++++++++++++++ serlcd/serlcd_test.go | 387 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 694 insertions(+), 3 deletions(-) create mode 100644 serlcd/example_test.go create mode 100644 serlcd/serlcd.go create mode 100644 serlcd/serlcd_test.go diff --git a/go.mod b/go.mod index e648eb9..08b6125 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/maruel/ansi256 v1.0.2 github.com/mattn/go-colorable v0.1.13 golang.org/x/image v0.23.0 - periph.io/x/conn/v3 v3.7.1 + periph.io/x/conn/v3 v3.7.2 periph.io/x/host/v3 v3.8.3 ) diff --git a/go.sum b/go.sum index ddaba69..3b2d8ce 100644 --- a/go.sum +++ b/go.sum @@ -15,7 +15,7 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -periph.io/x/conn/v3 v3.7.1 h1:tMjNv3WO8jEz/ePuXl7y++2zYi8LsQ5otbmqGKy3Myg= -periph.io/x/conn/v3 v3.7.1/go.mod h1:c+HCVjkzbf09XzcqZu/t+U8Ss/2QuJj0jgRF6Nye838= +periph.io/x/conn/v3 v3.7.2 h1:qt9dE6XGP5ljbFnCKRJ9OOCoiOyBGlw7JZgoi72zZ1s= +periph.io/x/conn/v3 v3.7.2/go.mod h1:Ao0b4sFRo4QOx6c1tROJU1fLJN1hUIYggjOrkIVnpGg= periph.io/x/host/v3 v3.8.3 h1:v90ozCFDWgEyfNElZ+JnOvq0jAdW0vmgjCUy8dYXDds= periph.io/x/host/v3 v3.8.3/go.mod h1:uKrIpfXjELwHkwGBNe6aos//XiQ/3uxDa1P2BmLV6Ok= diff --git a/serlcd/example_test.go b/serlcd/example_test.go new file mode 100644 index 0000000..2cb12a4 --- /dev/null +++ b/serlcd/example_test.go @@ -0,0 +1,50 @@ +// Copyright 2024 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package serlcd_test + +import ( + "errors" + "fmt" + "log" + "time" + + "periph.io/x/conn/v3/display" + "periph.io/x/conn/v3/display/displaytest" + "periph.io/x/conn/v3/i2c" + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/devices/v3/serlcd" + "periph.io/x/host/v3" +) + +func Example() { + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + bus, err := i2creg.Open("") + if err != nil { + log.Fatal(err) + } + defer bus.Close() + + conn := &i2c.Dev{Bus: bus, Addr: serlcd.DefaultI2CAddress} + dev := serlcd.NewConn(conn, 4, 20) + _ = dev.Clear() + n, err := dev.WriteString("Hello") + fmt.Printf("n=%d, err=%s\n", n, err) + + time.Sleep(10 * time.Second) + + fmt.Println("calling TestTextDisplay") + + errs := displaytest.TestTextDisplay(dev, true) + fmt.Println("back from TestTextDsiplay") + for _, e := range errs { + if !errors.Is(e, display.ErrNotImplemented) { + fmt.Println(e) + } + } + +} diff --git a/serlcd/serlcd.go b/serlcd/serlcd.go new file mode 100644 index 0000000..93a6c72 --- /dev/null +++ b/serlcd/serlcd.go @@ -0,0 +1,254 @@ +// Copyright 2024 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +// This package provides an implementation for the SparkFun SerLCD intelligent +// LCD display. This display provides hardware interfaces for SPI, I2C, and +// UART. Implements conn.display.TextDisplay +package serlcd + +import ( + "errors" + "fmt" + "io" + "time" + + "periph.io/x/conn/v3" + "periph.io/x/conn/v3/display" +) + +// Representation of a SerLCD display. +type Dev struct { + conn conn.Conn + w io.Writer + cols int + rows int + // Display on/off, Curosr, Blink + displayDCB byte +} + +const ( + // For i2c there's a buffer limitation of 32 bytes. + // See this issue: + // + // https://github.com/sparkfun/OpenLCD/issues/29 + // + // Writing more than that will lock the device up necessitating + // a device reset. + _MAX_I2C_WRITE = 32 + DefaultI2CAddress uint16 = 0x72 +) + +var settingMode byte = 0x7c +var cmdMode byte = 0xfe +var clear = []byte{settingMode, 0x2d} + +func wrap(err error) error { + return fmt.Errorf("serlcd: %w", err) +} + +// Create a SerLCD display using a hardware interface that provides io.Writer. +// That can be the i2c.Bus, or a 3rd Party UART library for serial +// communications. +func NewSerLCD(writer io.Writer, rows, cols int) *Dev { + dev := &Dev{w: writer, rows: rows, cols: cols} + _ = dev.Display(true) + return dev +} + +// Create a SerLCD display using a hardware interface that provides +// conn.Conn. For example, a conn.spi.Conn +func NewConn(conn conn.Conn, rows, cols int) *Dev { + dev := &Dev{conn: conn, rows: rows, cols: cols, w: nil} + return dev +} + +// Enable/Disable auto scroll +func (dev *Dev) AutoScroll(enabled bool) (err error) { + return wrap(display.ErrNotImplemented) +} + +// Return the number of columns the display supports +func (dev *Dev) Cols() int { + return dev.cols +} + +// Clear the display and move the cursor home. +func (dev *Dev) Clear() (err error) { + _, err = dev.Write(clear) + time.Sleep(2 * time.Millisecond) + return +} + +// Set the cursor mode. You can pass multiple arguments. +// Cursor(CursorOff, CursorUnderline) +func (dev *Dev) Cursor(mode ...display.CursorMode) (err error) { + dev.displayDCB &= 0x04 + for _, cmd := range mode { + switch cmd { + case display.CursorBlink: + dev.displayDCB |= 0x01 + case display.CursorUnderline: + dev.displayDCB |= 0x02 + case display.CursorBlock: + dev.displayDCB |= 0x01 + case display.CursorOff: + default: + err = wrap(display.ErrInvalidCommand) + return + } + } + dev.displayDCB = (dev.displayDCB | 0x08) & 0xf + + _, err = dev.Write([]byte{cmdMode, dev.displayDCB}) + return +} + +// Halt shuts down the display. If the IO source implements io.Closer, it is +// called. +func (dev *Dev) Halt() (err error) { + err = dev.Clear() + if err != nil { + return + } + err = dev.Display(false) + if err != nil { + return + } + if dev.w != nil { + if cl, ok := dev.w.(io.Closer); ok { + err = cl.Close() + } + } + return +} + +// Move the cursor home (MinRow(),MinCol()) +func (dev *Dev) Home() (err error) { + err = dev.MoveTo(dev.MinRow(), dev.MinCol()) + time.Sleep(2 * time.Millisecond) + return +} + +// Return the min column position. +func (dev *Dev) MinCol() int { + return 0 +} + +// Return the min row position. +func (dev *Dev) MinRow() int { + return 0 +} + +// Move the cursor forward or backward. +func (dev *Dev) Move(dir display.CursorDirection) (err error) { + cmdByte := byte(0x10) + switch dir { + case display.Backward: + // Nothing + case display.Forward: + cmdByte |= 0x04 + case display.Down: + fallthrough + case display.Up: + fallthrough + default: + err = wrap(display.ErrNotImplemented) + return + } + _, err = dev.Write([]byte{cmdMode, cmdByte}) + return +} + +// Move the cursor to an arbitrary position. +func (dev *Dev) MoveTo(row, col int) (err error) { + lineOffsets := []byte{0, 64, 20, 84} + if row < dev.MinRow() || row >= dev.Rows() || + col < dev.MinCol() || col >= dev.Cols() { + return errors.New("serlcd: invalid MoveTo() offset") + } + cmdByte := byte(0x80) + lineOffsets[row] + byte(col) + _, err = dev.Write([]byte{cmdMode, byte(cmdByte)}) + return +} + +// Return the number of rows the display supports. +func (dev *Dev) Rows() int { + return dev.rows +} + +// Turn the display on / off +func (dev *Dev) Display(on bool) (err error) { + if on { + dev.displayDCB |= 0x04 + } else { + dev.displayDCB ^= 0x04 + } + _, err = dev.Write([]byte{cmdMode, (dev.displayDCB | 0x08) & 0x0f}) + return +} + +// return info about the display. +func (dev *Dev) String() string { + ioType := "None" + if dev.conn != nil { + ioType = "periph.io.Conn.Conn" + } else if dev.w != nil { + ioType = fmt.Sprintf("%#v", dev.w) + } + return fmt.Sprintf("SparkFun SerLCD %dx%d Display - %s", dev.cols, dev.rows, ioType) +} + +// Write a set of bytes to the display. +func (dev *Dev) Write(p []byte) (n int, err error) { + if dev.w != nil { + n, err = dev.w.Write(p) + return + } + for n < len(p) { + bytesToWrite := len(p) - n + if bytesToWrite > _MAX_I2C_WRITE { + bytesToWrite = _MAX_I2C_WRITE + } + w := p[n : n+bytesToWrite] + err = dev.conn.Tx(w, nil) + if err != nil { + break + } + n = n + bytesToWrite + time.Sleep(time.Duration(40*bytesToWrite) * time.Microsecond) + } + + return +} + +// Write a string output to the display. +func (dev *Dev) WriteString(text string) (n int, err error) { + n, err = dev.Write([]byte(text)) + return +} + +// Set the backlight intensity with 0 being off, and 255 being maximum. +func (dev *Dev) Backlight(intensity display.Intensity) error { + return dev.RGBBacklight(intensity, intensity, intensity) +} + +// Set the character contrast on the device. Writes to EEPROM, so this should +// be used sparingly. The default device Contrast is 40. +func (dev *Dev) Contrast(contrast display.Contrast) error { + _, err := dev.Write([]byte{settingMode, 0x18, byte(contrast)}) + return err +} + +// Set the backlight color with 0 being off, and 255 being maximum intensity +// for each color. +func (dev *Dev) RGBBacklight(red, green, blue display.Intensity) error { + _, err := dev.Write([]byte{settingMode, 0x2b, byte(red & 0xff), byte(green & 0xff), byte(blue & 0xff)}) + return err +} + +var _ display.TextDisplay = &Dev{} +var _ display.DisplayContrast = &Dev{} +var _ display.DisplayBacklight = &Dev{} +var _ display.DisplayRGBBacklight = &Dev{} +var _ conn.Resource = &Dev{} diff --git a/serlcd/serlcd_test.go b/serlcd/serlcd_test.go new file mode 100644 index 0000000..dd1b80c --- /dev/null +++ b/serlcd/serlcd_test.go @@ -0,0 +1,387 @@ +// Copyright 2024 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package serlcd + +import ( + "errors" + "fmt" + "os" + "testing" + "time" + + "periph.io/x/conn/v3/display" + "periph.io/x/conn/v3/display/displaytest" + "periph.io/x/conn/v3/i2c" + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/conn/v3/i2c/i2ctest" + "periph.io/x/host/v3" +) + +var liveDevice bool = false +var bus i2c.Bus +var eepromTests bool = false +var sleepDuration time.Duration = time.Millisecond + +// Playback for interface tests. +var pbInterface = []i2ctest.IO{ + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x70, 0x61, 0x72, 0x6b, 0x46, 0x75, 0x6e, 0x20, 0x53, 0x65, 0x72, 0x4c, 0x43, 0x44, 0x20, 0x32, 0x30, 0x78, 0x34, 0x20, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x20, 0x2d, 0x20, 0x70}}, + {Addr: DefaultI2CAddress, W: []uint8{0x65, 0x72, 0x69, 0x70, 0x68, 0x2e, 0x69, 0x6f, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x6e}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x41, 0x75, 0x74, 0x6f, 0x20, 0x53, 0x63, 0x72, 0x6f, 0x6c, 0x6c, 0x20, 0x54, 0x65, 0x73, 0x74}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x80}}, + {Addr: DefaultI2CAddress, W: []uint8{0x41}}, + {Addr: DefaultI2CAddress, W: []uint8{0x42}}, + {Addr: DefaultI2CAddress, W: []uint8{0x43}}, + {Addr: DefaultI2CAddress, W: []uint8{0x44}}, + {Addr: DefaultI2CAddress, W: []uint8{0x45}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x47}}, + {Addr: DefaultI2CAddress, W: []uint8{0x48}}, + {Addr: DefaultI2CAddress, W: []uint8{0x49}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4a}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4c}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4e}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4f}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x51}}, + {Addr: DefaultI2CAddress, W: []uint8{0x52}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53}}, + {Addr: DefaultI2CAddress, W: []uint8{0x54}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x41}}, + {Addr: DefaultI2CAddress, W: []uint8{0x42}}, + {Addr: DefaultI2CAddress, W: []uint8{0x43}}, + {Addr: DefaultI2CAddress, W: []uint8{0x44}}, + {Addr: DefaultI2CAddress, W: []uint8{0x45}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x47}}, + {Addr: DefaultI2CAddress, W: []uint8{0x48}}, + {Addr: DefaultI2CAddress, W: []uint8{0x49}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4a}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4c}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4e}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4f}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x51}}, + {Addr: DefaultI2CAddress, W: []uint8{0x52}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53}}, + {Addr: DefaultI2CAddress, W: []uint8{0x54}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x94}}, + {Addr: DefaultI2CAddress, W: []uint8{0x41}}, + {Addr: DefaultI2CAddress, W: []uint8{0x42}}, + {Addr: DefaultI2CAddress, W: []uint8{0x43}}, + {Addr: DefaultI2CAddress, W: []uint8{0x44}}, + {Addr: DefaultI2CAddress, W: []uint8{0x45}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x47}}, + {Addr: DefaultI2CAddress, W: []uint8{0x48}}, + {Addr: DefaultI2CAddress, W: []uint8{0x49}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4a}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4c}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4e}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4f}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x51}}, + {Addr: DefaultI2CAddress, W: []uint8{0x52}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53}}, + {Addr: DefaultI2CAddress, W: []uint8{0x54}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xd4}}, + {Addr: DefaultI2CAddress, W: []uint8{0x41}}, + {Addr: DefaultI2CAddress, W: []uint8{0x42}}, + {Addr: DefaultI2CAddress, W: []uint8{0x43}}, + {Addr: DefaultI2CAddress, W: []uint8{0x44}}, + {Addr: DefaultI2CAddress, W: []uint8{0x45}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x47}}, + {Addr: DefaultI2CAddress, W: []uint8{0x48}}, + {Addr: DefaultI2CAddress, W: []uint8{0x49}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4a}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4c}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4e}}, + {Addr: DefaultI2CAddress, W: []uint8{0x4f}}, + {Addr: DefaultI2CAddress, W: []uint8{0x20}}, + {Addr: DefaultI2CAddress, W: []uint8{0x51}}, + {Addr: DefaultI2CAddress, W: []uint8{0x52}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53}}, + {Addr: DefaultI2CAddress, W: []uint8{0x54}}, + {Addr: DefaultI2CAddress, W: []uint8{0x61, 0x75, 0x74, 0x6f, 0x20, 0x73, 0x63, 0x72, 0x6f, 0x6c, 0x6c, 0x20, 0x68, 0x61, 0x70, 0x70, 0x65, 0x6e}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x41, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x20, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x80}}, + {Addr: DefaultI2CAddress, W: []uint8{0x28, 0x30, 0x2c, 0x30, 0x29}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc1}}, + {Addr: DefaultI2CAddress, W: []uint8{0x28, 0x31, 0x2c, 0x31, 0x29}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x96}}, + {Addr: DefaultI2CAddress, W: []uint8{0x28, 0x32, 0x2c, 0x32, 0x29}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xd7}}, + {Addr: DefaultI2CAddress, W: []uint8{0x28, 0x33, 0x2c, 0x33, 0x29}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x4f, 0x66, 0x66}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x55, 0x6e, 0x64, 0x65, 0x72, 0x6c, 0x69, 0x6e, 0x65}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xe}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x42, 0x6c, 0x6f, 0x63, 0x6b}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xd}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x42, 0x6c, 0x69, 0x6e, 0x6b}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xd}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x54, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x3e}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x14}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x14}}, + {Addr: DefaultI2CAddress, W: []uint8{0x30}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x31}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x32}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x33}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x34}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x35}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x36}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x37}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x38}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x39}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x10}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x64, 0x65, 0x76, 0x20, 0x6f, 0x66, 0x66}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0x8}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x64, 0x65, 0x76, 0x20, 0x6f, 0x6e}}} + +var pbBacklight = []i2ctest.IO{ + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x42, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x20, 0x4f, 0x66, 0x66}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2b, 0x0, 0x0, 0x0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2b, 0xff, 0xff, 0xff}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x42, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x20, 0x4f, 0x6e}}} + +var pbRGBBacklight = []i2ctest.IO{ + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x42, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x20, 0x5b, 0x5d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x74}}, + {Addr: DefaultI2CAddress, W: []uint8{0x79, 0x7b, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x30, 0x2c, 0x20, 0x30, 0x7d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2b, 0xff, 0x0, 0x0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x42, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x20, 0x5b, 0x5d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x74}}, + {Addr: DefaultI2CAddress, W: []uint8{0x79, 0x7b, 0x30, 0x2c, 0x20, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x30, 0x7d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2b, 0x0, 0xff, 0x0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x42, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x20, 0x5b, 0x5d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x74}}, + {Addr: DefaultI2CAddress, W: []uint8{0x79, 0x7b, 0x30, 0x2c, 0x20, 0x30, 0x2c, 0x20, 0x32, 0x35, 0x35, 0x7d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2b, 0x0, 0x0, 0xff}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2b, 0xff, 0xff, 0xff}}} + +var pbContrast = []i2ctest.IO{ + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x73, 0x74, 0x20, 0x35}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x18, 0x5}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc0}}, + {Addr: DefaultI2CAddress, W: []uint8{0x53, 0x65, 0x74, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x73, 0x74, 0x20, 0x30, 0x78, 0x38, 0x30}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x18, 0x80}}, + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x18, 0x28}}} + +var pbHalt = []i2ctest.IO{ + {Addr: DefaultI2CAddress, W: []uint8{0x7c, 0x2d}}, + {Addr: DefaultI2CAddress, W: []uint8{0xfe, 0xc}}} + +func init() { + var err error + // If the environment variable is set, assume we have a live device on + // the default i2c bus and use it for testing. If the variable is not + // present, then use the playback/read values. + liveDevice = os.Getenv("SERLCD") != "" + // If EEPROM is set, then tests that write to EEPROM (display intensity / + // contrast are tested if it's a live test. + eepromTests = os.Getenv("EEPROM") != "" + if _, err = host.Init(); err != nil { + fmt.Println(err) + } + + if liveDevice { + sleepDuration = 5 * time.Second + bus, err = i2creg.Open("") + if err != nil { + fmt.Println(err) + } + // Add the recorder to dump the data stream when we're using a live device. + bus = &i2ctest.Record{Bus: bus} + } else { + bus = &i2ctest.Playback{DontPanic: true} + } + +} + +// getDev returns a SerLCD device for testing connected to either a live +// bus, or a playback bus. playbackOps is a slice of i2ctest.IO +// operations to be used for playback mode. Ignored for live device +// testing. +func getDev(t *testing.T, playbackOps ...[]i2ctest.IO) (*Dev, error) { + if liveDevice { + if recorder, ok := bus.(*i2ctest.Record); ok { + // Clear the operations buffer. + recorder.Ops = make([]i2ctest.IO, 0, 32) + } + } else { + if len(playbackOps) == 1 { + pb := bus.(*i2ctest.Playback) + pb.Ops = playbackOps[0] + pb.Count = 0 + } + } + conn := &i2c.Dev{Bus: bus, Addr: DefaultI2CAddress} + dev := NewConn(conn, 4, 20) + + return dev, nil +} + +// shutdown dumps the recorder values if we we're running a live device. +func shutdown(t *testing.T) { + if recorder, ok := bus.(*i2ctest.Record); ok { + t.Logf("%#v", recorder.Ops) + } +} + +// This tests all functions in the TextDisplay interface. +func TestInterface(t *testing.T) { + dev, err := getDev(t, pbInterface) + defer shutdown(t) + if err != nil { + t.Fatal(err) + } + errs := displaytest.TestTextDisplay(dev, liveDevice) + for _, err := range errs { + if !errors.Is(err, display.ErrNotImplemented) { + t.Error(err) + } + } +} + +// TestBacklight verifies the backlight turns off. +func TestBacklight(t *testing.T) { + if liveDevice && !eepromTests { + return + } + dev, err := getDev(t, pbBacklight) + defer shutdown(t) + if err != nil { + t.Fatal(err) + } + _ = dev.Clear() + _, err = dev.WriteString("Set Backlight Off") + if err != nil { + t.Error(err) + } + + err = dev.Backlight(0) + if err != nil { + t.Error(err) + } + time.Sleep(sleepDuration) + _ = dev.Clear() + err = dev.Backlight(0xff) + if err != nil { + t.Error(err) + } + _, _ = dev.WriteString("Set Backlight On") + time.Sleep(sleepDuration) +} + +// TestRGBBacklight tests the RGB Backlight works as expected +func TestRGBBacklight(t *testing.T) { + if liveDevice && !eepromTests { + return + } + dev, err := getDev(t, pbRGBBacklight) + defer shutdown(t) + if err != nil { + t.Fatal(err) + } + _ = dev.Clear() + for ix := range 3 { + w := make([]display.Intensity, 3) + w[ix] = 0xff + _, err = dev.WriteString(fmt.Sprintf("Set Backlight %#v", w)) + if err != nil { + t.Error(err) + } + err = dev.RGBBacklight(w[0], w[1], w[2]) + if err != nil { + t.Error(err) + } + time.Sleep(sleepDuration) + } + err = dev.RGBBacklight(0xff, 0xff, 0xff) + if err != nil { + t.Error(err) + } +} + +// TestContrast() checks contrast operations. +func TestContrast(t *testing.T) { + if liveDevice && !eepromTests { + return + } + dev, err := getDev(t, pbContrast) + defer shutdown(t) + if err != nil { + t.Fatal(err) + } + _ = dev.Clear() + _, _ = dev.WriteString("Set Contrast 5") + err = dev.Contrast(5) + if err != nil { + t.Error(err) + } + _ = dev.MoveTo(dev.MinRow()+1, dev.MinCol()) + time.Sleep(sleepDuration) + _, _ = dev.WriteString("Set Contrast 0x80") + time.Sleep(sleepDuration) + err = dev.Contrast(0x80) + if err != nil { + t.Error(err) + } + time.Sleep(sleepDuration) + _ = dev.Contrast(40) +} + +func TestHalt(t *testing.T) { + dev, err := getDev(t, pbHalt) + defer shutdown(t) + if err != nil { + t.Fatal(err) + } + err = dev.Halt() + if err != nil { + t.Error(err) + } +}