Initial Add

pull/93/head
George Sexton 1 year ago
parent cad22759dd
commit 60a61ef359

@ -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 matrixorbital_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/matrixorbital"
"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: 0x11}
dev := matrixorbital.NewConnLK2047T(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)
}
}
}

@ -0,0 +1,377 @@
// 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 interface to MatrixOrbital Character LCD displays.
// The LK2047T display is compatible with the Adafruit USB-LCD Backpack.
package matrixorbital
import (
"errors"
"fmt"
"io"
"sync"
// "time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/gpio"
)
// Constants for programmable LEDs on some models.
type LEDColor int
const (
// Constants for colors supported by LEDs.
Off LEDColor = iota
Red
Green
Yellow
)
// The LK2047T is a basic MatrixOrbital LCD display. It's a 20x4 LCD with a
// keypad and LEDs.
//
// Implements periph.io/x/conn/v3/display.TextDisplay, Backlight, and
// DisplayContrast.
type LK2047T struct {
// Pins represents the set of gpio.PinOut pins exposed by the device. For
// units with LEDS, the pins are used to manipulate them. For the Adafruit
// USB/LCD backpack, 4 pins are exposed.
Pins []gpio.PinOut
d conn.Conn
writer io.Writer
mu sync.Mutex
rows int
cols int
chKeyboard chan byte
shutdown chan struct{}
}
type GPOEnabledDisplay interface {
// SetGPO turns a GPO pin on or off
GPO(pin int, l gpio.Level) error
}
// Command byte values used by the display. This only implements a subset of
// commands.
var cmdByte byte = 0xfe
var autoScrollOff = []byte{cmdByte, 0x52}
var autoScrollOn = []byte{cmdByte, 0x51}
var blockCursorOff = []byte{cmdByte, 0x54}
var blockCursorOn = []byte{cmdByte, 0x53}
var clearScreen = []byte{cmdByte, 0x58}
var cursorBack = []byte{cmdByte, 0x4c}
var cursorBlinkOff = []byte{cmdByte, 0x54}
var cursorBlinkOn = []byte{cmdByte, 0x53}
var cursorForward = []byte{cmdByte, 0x4d}
var displayOff = []byte{cmdByte, 0x46}
var displayOn = []byte{cmdByte, 0x42}
var goHome = []byte{cmdByte, 0x48}
var keypadBacklightOff = []byte{cmdByte, 0x98}
var setBrightness = []byte{cmdByte, 0x99}
var setContrast = []byte{cmdByte, 0x50}
var setCursorPosition = []byte{cmdByte, 0x47}
var setGPOOn = []byte{cmdByte, 0x57}
var setGPOOff = []byte{cmdByte, 0x56}
var underlineCursorOff = []byte{cmdByte, 0x4b}
var underlineCursorOn = []byte{cmdByte, 0x4a}
func wrapErr(err error) error {
if err == nil {
return nil
}
return fmt.Errorf("lk2047t: %w", err)
}
// Create a new LCD device using a periph.io/conn/Conn
func NewConnLK2047T(conn conn.Conn, rows, cols int) *LK2047T {
dev := &LK2047T{d: conn, rows: rows, cols: cols, Pins: make([]gpio.PinOut, 6)}
a := GPOEnabledDisplay(dev)
makePins(&a, dev.Pins)
return dev
}
// Create a new LCD device using an io.Writer. If your display is connected
// using a hardware interface that periph.io doesn't support (e.g. UART),
// you can still use this package as long as the hardware interface provides
// the io.Writer interface. rows is the number of lines the display supports,
// and cols is the character width of the device.
func NewWriterLK2047T(writer io.Writer, rows, cols int) *LK2047T {
dev := &LK2047T{writer: writer, rows: rows, cols: cols, Pins: make([]gpio.PinOut, 6)}
a := GPOEnabledDisplay(dev)
makePins(&a, dev.Pins)
return dev
}
// Enable or disable AutoScroll.
func (dev *LK2047T) AutoScroll(enabled bool) (err error) {
if enabled {
_, err = dev.Write(autoScrollOn)
} else {
_, err = dev.Write(autoScrollOff)
}
return
}
// Clears the screen, and moves the cursor to the home position.
func (dev *LK2047T) Clear() (err error) {
_, err = dev.Write(clearScreen)
if err == nil {
err = dev.Home()
}
return
}
// Return the number of columns supported by the device.
func (dev *LK2047T) Cols() int {
return dev.cols
}
// Set the cursor mode. E.G. underline, block, etc.
func (dev *LK2047T) Cursor(modes ...display.CursorMode) (err error) {
for _, mode := range modes {
switch mode {
case display.CursorOff:
_, err = dev.Write(blockCursorOff)
if err == nil {
_, err = dev.Write(underlineCursorOff)
}
if err == nil {
_, err = dev.Write(cursorBlinkOff)
}
case display.CursorUnderline:
_, err = dev.Write(underlineCursorOn)
case display.CursorBlock:
_, err = dev.Write(blockCursorOn)
case display.CursorBlink:
_, err = dev.Write(cursorBlinkOn)
default:
err = fmt.Errorf("lk2047t: invalid cursor mode %d", mode)
}
if err != nil {
break
}
}
return
}
// Halt shuts down the display, and closes the output device if it implements
// io.Closer. If a keypad read operation is running, closing the device will
// terminate it.
func (dev *LK2047T) Halt() (err error) {
err = dev.Display(false)
_ = dev.KeypadBacklight(false)
if err != nil {
return err
}
dev.mu.Lock()
defer dev.mu.Unlock()
if dev.shutdown != nil {
dev.shutdown <- struct{}{}
}
var cl io.Closer
var ok bool
if dev.d != nil {
cl, ok = dev.d.(io.Closer)
} else {
cl, ok = dev.writer.(io.Closer)
}
if ok {
err = cl.Close()
} else {
err = errors.New("output connection doesn't support io.Closer()")
}
err = wrapErr(err)
return
}
// Home resets the cursor to the default position.
func (dev *LK2047T) Home() (err error) {
_, err = dev.Write(goHome)
return
}
// MinCol returns the numbering scheme of the device's minimum column number.
// Generally, it will be 0 or 1
func (dev *LK2047T) MinCol() int {
return 1
}
// MinRow returns the numbering scheme of the device's minimum row (line)
// number. Generally, it will be 0 or 1.
func (dev *LK2047T) MinRow() int {
return 1
}
// Move the cursor forward or backwards.
func (dev *LK2047T) Move(direction display.CursorDirection) (err error) {
switch direction {
case display.Forward:
_, err = dev.Write(cursorForward)
case display.Backward:
_, err = dev.Write(cursorBack)
case display.Up:
case display.Down:
default:
err = errors.New("lk2047t: invalid move direction.")
}
return
}
// Move the cursor to an arbitrary row/column on the device.
func (dev *LK2047T) MoveTo(row, col int) (err error) {
if row < 1 || row > dev.rows || col < 1 || col > dev.cols {
return fmt.Errorf("lk2047t: MoveTo(%d, %d) value out of range.", row, col)
}
_, err = dev.Write([]byte{setCursorPosition[0], setCursorPosition[1], byte(col), byte(row)})
return err
}
// ReadKeypad reads from the displays built-in keypad. The io device used by the
// display must implement io.Reader. If it does not, then an error is returned.
func (dev *LK2047T) ReadKeypad() (<-chan byte, error) {
dev.mu.Lock()
defer dev.mu.Unlock()
if dev.chKeyboard != nil {
return dev.chKeyboard, nil
}
var rdr io.Reader
var ok bool
if dev.writer == nil {
rdr, ok = dev.d.(io.Reader)
} else {
rdr, ok = dev.writer.(io.Reader)
}
if !ok {
return nil, errors.New("lk2047t: output device does not implement io.Reader")
}
dev.chKeyboard = make(chan byte, 8)
dev.shutdown = make(chan struct{})
go func() {
defer func() {
dev.mu.Lock()
close(dev.chKeyboard)
dev.chKeyboard = nil
dev.mu.Unlock()
}()
buf := make([]byte, 4)
var err error
var n int
for err == nil {
select {
case <-dev.shutdown:
return
default:
n, err = rdr.Read(buf)
if n > 0 {
for ix := range n {
dev.chKeyboard <- buf[ix]
}
}
}
}
}()
return dev.chKeyboard, nil
}
// Return the number of rows supported by the device.
func (dev *LK2047T) Rows() int {
return dev.rows
}
// Set the intensity of the backlight. Refer to the docs in the lcd package
// for warnings on this function. Provides periph.io/x/conn/v3/display.Backlight
func (dev *LK2047T) Backlight(intensity display.Intensity) error {
_, err := dev.Write([]byte{setBrightness[0], setBrightness[1], byte(intensity)})
return err
}
// Set the constrast of the display. Refer to the docs in the lcd package
// for warnings on this function. Provides periph.io/x/conn/v3/display.DisplayContrast
func (dev *LK2047T) Contrast(contrast display.Contrast) error {
_, err := dev.Write([]byte{setContrast[0], setContrast[1], byte(contrast)})
return err
}
// Set the display on or off.
func (dev *LK2047T) Display(on bool) (err error) {
if on {
_, err = dev.Write([]byte{displayOn[0], displayOn[1], 0})
} else {
_, err = dev.Write(displayOff)
}
return
}
func (dev *LK2047T) KeypadBacklight(on bool) error {
if on {
return dev.Display(on)
}
_, err := dev.Write(keypadBacklightOff)
return err
}
// Set the specified output pin state.
func (dev *LK2047T) GPO(pin int, on gpio.Level) (err error) {
if on {
_, err = dev.Write([]byte{setGPOOn[0], setGPOOn[1], byte(pin)})
} else {
_, err = dev.Write([]byte{setGPOOff[0], setGPOOff[1], byte(pin)})
}
return
}
// Set an led to a supported color. number is 0 based.
func (dev *LK2047T) LED(number int, color LEDColor) error {
if color < Off || color > Yellow {
return fmt.Errorf("lk2047t: invalid color: %d", color)
}
err := dev.Pins[number*2].Out(gpio.Level(color&Red == Red))
if err != nil {
return err
}
err = dev.Pins[number*2+1].Out(gpio.Level(color&Green == Green))
return err
}
func (dev *LK2047T) String() string {
var ioType any
if dev.d != nil {
ioType = dev.d
} else {
ioType = dev.writer
}
return fmt.Sprintf("MatrixOrbital LK204-7T LCD Display: Rows: %d Cols: %d Connection: %T", dev.rows, dev.cols, ioType)
}
// Write commands or data to the display
func (dev *LK2047T) Write(p []byte) (n int, err error) {
dev.mu.Lock()
defer dev.mu.Unlock()
if dev.writer == nil {
err = dev.d.Tx(p, nil)
n = len(p)
} else {
n, err = dev.writer.Write(p)
}
err = wrapErr(err)
return
}
// WriteString sends a text string to the display.
func (dev *LK2047T) WriteString(text string) (int, error) {
n, err := dev.Write([]byte(text))
return n, err
}
var _ display.TextDisplay = &LK2047T{}
var _ GPOEnabledDisplay = &LK2047T{}
var _ display.DisplayContrast = &LK2047T{}
var _ display.DisplayBacklight = &LK2047T{}
var _ conn.Resource = &LK2047T{}

@ -0,0 +1,191 @@
// 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 matrixorbital
import (
"fmt"
"hash"
"hash/crc32"
"io"
"math/rand"
"sync/atomic"
"testing"
"time"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/display/displaytest"
)
type mockReadWriterCloser struct {
closed bool
bytesWritten int
bytesRead int
readChars string
hash hash.Hash32
// TODO: Add CRC32 verification of written bytes...
}
func (mr *mockReadWriterCloser) Read(p []byte) (n int, err error) {
if mr.closed {
err = io.EOF
}
cPos := rand.Intn(len(mr.readChars))
p[0] = byte(mr.readChars[cPos])
n = 1
mr.bytesRead += 1
return
}
func (mr *mockReadWriterCloser) Write(p []byte) (n int, err error) {
if mr.closed {
err = io.EOF
return
}
n = len(p)
mr.hash.Write(p)
mr.bytesWritten += n
return
}
func (mr *mockReadWriterCloser) Close() error {
mr.closed = true
return nil
}
// Shutdown accepts an expected hash. If expectedHash doesn't match the hash of
// the stream written to the mock, it writes an error to t. If the test changes,
// or the implementation of the device changes, you'll have to update the
// expectedHash value in the Shutdown() call.
func (mr *mockReadWriterCloser) Shutdown(t *testing.T, expectedHash uint32) {
finalHash := mr.hash.Sum32()
if expectedHash == 0 {
t.Logf("Mock Reader: Final Hash=0x%x", finalHash)
} else if finalHash != expectedHash {
t.Errorf("Incorrect hash. Expected: 0x%x, received 0x%x", expectedHash, finalHash)
}
}
// getDisplay returns an LCD display constructed with a mock read/writer for
// testing.
func getDisplay() (*LK2047T, *mockReadWriterCloser) {
wr := &mockReadWriterCloser{readChars: "ABCDEGH", hash: crc32.NewIEEE()}
return NewWriterLK2047T(wr, 4, 20), wr
}
func TestTextDisplay(t *testing.T) {
fmt.Println("beginning tests")
dev, mock := getDisplay()
defer mock.Shutdown(t, 0x4b8e39ef)
wrTest := "abcdef"
nWritten, err := dev.WriteString(wrTest)
if err != nil {
t.Error(err)
}
if nWritten != len(wrTest) || mock.bytesWritten != len(wrTest) {
t.Errorf("write string error wrote %d bytes expected %d", mock.bytesWritten, len(wrTest))
}
}
func TestInterface(t *testing.T) {
dev, mock := getDisplay()
defer mock.Shutdown(t, 0x1d42cd75)
errors := displaytest.TestTextDisplay(dev, false)
for _, err := range errors {
if err != display.ErrNotImplemented {
t.Error(err)
}
}
if mock.bytesWritten == 27 {
t.Error("27")
}
}
func TestLEDs(t *testing.T) {
dev, mock := getDisplay()
defer mock.Shutdown(t, 0x3d9030ea)
for ix := range 3 {
for color := Off; color <= Yellow; color++ {
err := dev.LED(ix, color)
if err != nil {
t.Error(err)
}
}
}
}
// Peform a basic test on optional interface methods.
func TestContrastBacklight(t *testing.T) {
dev, mock := getDisplay()
defer mock.Shutdown(t, 0x93097842)
if err := dev.Contrast(0); err != nil {
t.Error(err)
}
if err := dev.Contrast(100); err != nil {
t.Error(err)
}
if err := dev.Backlight(0); err != nil {
t.Error(err)
}
if err := dev.Backlight(50); err != nil {
t.Error(err)
}
if err := dev.KeypadBacklight(true); err != nil {
t.Error(err)
}
if err := dev.KeypadBacklight(false); err != nil {
t.Error(err)
}
}
func TestPins(t *testing.T) {
dev, _ := getDisplay()
for ix, pin := range dev.Pins {
if len(pin.String()) == 0 {
t.Errorf("pin %d return empty string", ix)
}
if pin.Number() != ix+1 {
t.Errorf("Pin %d, unexpected pin # %d", ix, pin.Number())
}
if len(pin.Name()) == 0 {
t.Errorf("pin %d returned empty name!", ix)
}
}
}
func TestKeypad(t *testing.T) {
dev, _ := getDisplay()
ch, err := dev.ReadKeypad()
if err != nil {
t.Fatal(err)
}
if ch == nil {
t.Fatal("ReadKeypad() returned nil channel!")
}
received := atomic.Int32{}
expected := int32(10)
go func() {
for {
if received.Load() >= expected {
if err := dev.Halt(); err != nil {
t.Error(err)
}
return
}
time.Sleep(time.Millisecond)
}
}()
for c := range ch {
t.Logf("received %s", string(c))
received.Add(1)
time.Sleep(5 * time.Millisecond)
}
}
var _ io.Reader = &mockReadWriterCloser{}
var _ io.Writer = &mockReadWriterCloser{}
var _ io.Closer = &mockReadWriterCloser{}

@ -0,0 +1,62 @@
// 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 matrixorbital
import (
"errors"
"fmt"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/physic"
)
// A gpoPin is an output pin that can be toggled on the display. On the
// LK204-7T-1U, there are six GPO pins wired to LEDs. On the Adafruit
// USB LCD Backpack, there are four bare pins exposed. gpoPin implements
// gpio.PinOut.
type gpoPin struct {
name string
number int
display *GPOEnabledDisplay
}
// A generic routine to create our set of pins.
func makePins(display *GPOEnabledDisplay, pins []gpio.PinOut) {
for ix := range len(pins) {
pin := &gpoPin{name: fmt.Sprintf("GPO%d", ix+1), number: ix + 1, display: display}
pins[ix] = pin
}
}
func (pin *gpoPin) Name() string {
return pin.name
}
func (pin *gpoPin) Number() int {
return pin.number
}
func (pin *gpoPin) String() string {
return fmt.Sprintf("matrixorbital Pin: Name: %s Number %d", pin.name, pin.number)
}
func (pin *gpoPin) Halt() error {
return nil
}
func (pin *gpoPin) Out(l gpio.Level) error {
d := *pin.display
return d.GPO(pin.number, l)
}
func (pin *gpoPin) Function() string {
return "Out"
}
func (pin *gpoPin) PWM(duty gpio.Duty, f physic.Frequency) error {
return errors.New("not implemented")
}
var _ gpio.PinOut = &gpoPin{}
Loading…
Cancel
Save