diff --git a/display/displaytest/text.go b/display/displaytest/text.go new file mode 100644 index 0000000..96958d5 --- /dev/null +++ b/display/displaytest/text.go @@ -0,0 +1,207 @@ +// 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 displaytest + +import ( + "errors" + "fmt" + "time" + + "periph.io/x/conn/v3/display" +) + +// TestTextDisplay exercises the methods provided by the interface. It can be +// used interactively as a quick smoke test of an implementation, and from test +// routines. This doesn't test brightness or contrast to avoid EEPROM wear +// issues. +func TestTextDisplay(dev display.TextDisplay, interactive bool) []error { + var result []error + var err error + + pauseTime := time.Duration(0) + if interactive { + pauseTime = 3 * time.Second + } + // Turn the dev on and write the String() value. + if err = dev.Display(true); err != nil { + result = append(result, err) + } + + if err = dev.Clear(); err != nil { + result = append(result, err) + } + + if _, err = dev.WriteString(dev.String()); err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + + if err = dev.Clear(); err != nil { + result = append(result, err) + } + _, err = dev.WriteString("Auto Scroll Test") + if err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + err = dev.AutoScroll(true) + if err != nil { + result = append(result, err) + } + // Test Display fill + for line := range dev.Rows() { + c := rune('A') + if err = dev.MoveTo(dev.MinRow()+line, dev.MinCol()); err != nil { + result = append(result, err) + } + for col := range dev.Cols() { + if col%5 == 0 && col > 0 { + _, err = dev.Write([]byte{byte(' ')}) + } else { + _, err = dev.Write([]byte{byte(c)}) + } + if err != nil { + result = append(result, err) + } + c = c + 1 + } + } + // Test AutoScroll working + time.Sleep(pauseTime) + nWritten, err := dev.WriteString("auto scroll happen") + if err != nil { + result = append(result, err) + } + if nWritten != 18 { + result = append(result, fmt.Errorf("dev.WriteString() expected %d chars written, received %d", 18, nWritten)) + } + time.Sleep(pauseTime) + if err = dev.AutoScroll(false); err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + + // Test Absolute Positioning + if err = dev.Clear(); err != nil { + result = append(result, err) + } + if _, err = dev.WriteString("Absolute Positioning"); err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + if err = dev.Clear(); err != nil { + result = append(result, err) + } + for ix := range dev.Rows() { + if err = dev.MoveTo(dev.MinRow()+ix, dev.MinCol()+ix); err != nil { + result = append(result, err) + } + if _, err = dev.WriteString(fmt.Sprintf("(%d,%d)", dev.MinRow()+ix, dev.MinCol()+ix)); err != nil { + result = append(result, err) + } + } + time.Sleep(pauseTime) + + // Test that MoveTo returns error for invalid coordinates + moveCases := []struct { + row int + col int + }{ + {row: dev.MinRow() - 1, col: dev.MinCol()}, + {row: dev.MinRow(), col: dev.MinCol() - 1}, + {row: dev.Rows() + 1, col: dev.Cols()}, + {row: dev.Rows(), col: dev.Cols() + 1}, + } + for _, tc := range moveCases { + if err = dev.MoveTo(tc.row, tc.col); err == nil { + result = append(result, fmt.Errorf("did not receive expected error on MoveTo(%d,%d)", tc.row, tc.col)) + } + } + + // Test Cursor Modes + if err = dev.Clear(); err != nil { + result = append(result, err) + } + modes := []string{"Off", "Underline", "Block", "Blink"} + for ix := display.CursorOff; ix <= display.CursorBlink; ix++ { + if err = dev.MoveTo(dev.MinRow()/2+1, dev.MinCol()); err != nil { + result = append(result, err) + } + if _, err = dev.WriteString("Cursor: " + modes[ix]); err != nil { + result = append(result, err) + } + if err = dev.Cursor(ix); err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + if err = dev.Cursor(display.CursorOff); err != nil { + result = append(result, err) + } + if err = dev.Clear(); err != nil { + result = append(result, err) + } + } + if err = dev.Cursor(display.CursorBlink + 1); err == nil { + result = append(result, errors.New("did not receive expected error on Cursor() with invalid value")) + } + + // Test Move Forward and Backward. 2 Should overwrite the 1 + if err = dev.Clear(); err != nil { + result = append(result, err) + } + if _, err = dev.WriteString("Testing >"); err != nil { + result = append(result, err) + } + if err = dev.Move(display.Forward); err != nil { + result = append(result, err) + } + if err = dev.Move(display.Forward); err != nil { + result = append(result, err) + } + for ix := range 10 { + if _, err = dev.WriteString(fmt.Sprintf("%d", ix)); err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + if err = dev.Move(display.Backward); err != nil { + result = append(result, err) + } + } + if err = dev.Move(display.Down + 1); err == nil { + result = append(result, errors.New("did not receive expected error on Move() with invalid value")) + } + + // Test Display on/off + if err = dev.Clear(); err != nil { + result = append(result, err) + } + if _, err = dev.WriteString("Set dev off"); err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + if err = dev.Display(false); err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + if err = dev.Display(true); err != nil { + result = append(result, err) + } + if err = dev.Clear(); err != nil { + result = append(result, err) + } + if _, err = dev.WriteString("Set dev on"); err != nil { + result = append(result, err) + } + time.Sleep(pauseTime) + + if interactive { + for _, e := range result { + if !errors.Is(err, display.ErrNotImplemented) { + fmt.Println(e) + } + } + } + return result +} diff --git a/display/text_display.go b/display/text_display.go new file mode 100644 index 0000000..ee0f566 --- /dev/null +++ b/display/text_display.go @@ -0,0 +1,105 @@ +// 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 display + +import ( + "errors" +) + +type CursorDirection int + +const ( + // Constants for moving the cursor relative to it's current position. + // + // Move the cursor one unit back. + Backward CursorDirection = iota + // Move the cursor one unit forward. + Forward + Up + Down +) + +type CursorMode int + +const ( + // Turn the cursor Off + CursorOff CursorMode = iota + // Enable Underline Cursor + CursorUnderline + // Enable Block Cursor + CursorBlock + // Blinking + CursorBlink +) + +// TextDisplay represents an interface to a basic character device. It provides +// standard methods implemented by a majority of character LCD devices. Pixel +// type displays can implement this interface if desired. +type TextDisplay interface { + // Enable/Disable auto scroll + AutoScroll(enabled bool) (err error) + // Return the number of columns the display supports + Cols() int + // Clear the display and move the cursor home. + Clear() (err error) + // Set the cursor mode. You can pass multiple arguments. + // Cursor(CursorOff, CursorUnderline) + // + // Implementations should return an error if the value of mode is not + // mode>= CursorOff && mode <= CursorBlink + Cursor(mode ...CursorMode) (err error) + // Move the cursor home (MinRow(),MinCol()) + Home() (err error) + // Return the min column position. + MinCol() int + // Return the min row position. + MinRow() int + // Move the cursor forward or backward. + Move(dir CursorDirection) (err error) + // Move the cursor to arbitrary position. Implementations should return an + // error if row < MinRow() || row > (Rows()-MinRow()), or col < MinCol() + // || col > (Cols()-MinCol()) + MoveTo(row, col int) (err error) + // Return the number of rows the display supports. + Rows() int + // Turn the display on / off + Display(on bool) (err error) + // return info about the display. + String() string + // Write a set of bytes to the display. + Write(p []byte) (n int, err error) + // Write a string output to the display. + WriteString(text string) (n int, err error) +} + +type Intensity int + +// Interface for displays that support a monochrome backlight. Displays that +// support RGB Backlights should implement this as well for maximum +// compatibility. +// +// Many units that support this command write the value to EEPROM, which has a +// finite number of writes. To turn the unit on/off, use TextDisplay.Display() +type DisplayBacklight interface { + Backlight(intensity Intensity) error +} + +// Interface for displays that support a RGB Backlight. E.G. the Sparkfun SerLCD +type DisplayRGBBacklight interface { + RGBBacklight(red, green, blue Intensity) error +} + +type Contrast int + +// Interface for displays that support a programmable contrast adjustment. +// As with SetBacklight(), many devices serialize the value to EEPROM, +// which support only a finite number of writes, so this should be used +// sparingly. +type DisplayContrast interface { + Contrast(contrast Contrast) error +} + +var ErrNotImplemented = errors.New("not implemented") +var ErrInvalidCommand = errors.New("invalid command")