summaryrefslogtreecommitdiff
path: root/cfa635/cfa635.go
diff options
context:
space:
mode:
authorGravatar Benjamin Barenblat <bbarenblat@gmail.com>2022-09-08 14:40:10 -0400
committerGravatar Benjamin Barenblat <bbarenblat@gmail.com>2022-09-08 14:40:10 -0400
commit64f73905537e4b1d083afe4c446279a78f9808c0 (patch)
treef7f22289cb0613dc7a29ac5ee5e87ca5ef61ec8c /cfa635/cfa635.go
parentd48c52c1298777cba68b6e78dc5cd83dda4f12de (diff)
Basic, read-only audiotrondHEADmain
Audiotrond exists and properly displays music being played through MPD. The display includes title, artist, album, and progress bar, and text scrolls if too long to fit on the screen. Backlight support exists; the display backlight fades smoothly on and off when the music starts and stops. After being stopped for a little while, audiotrond switches to a clock. Currently, audiotrond lacks user interaction support, so it keeps the keypad backlight off.
Diffstat (limited to 'cfa635/cfa635.go')
-rw-r--r--cfa635/cfa635.go226
1 files changed, 226 insertions, 0 deletions
diff --git a/cfa635/cfa635.go b/cfa635/cfa635.go
new file mode 100644
index 0000000..296ab21
--- /dev/null
+++ b/cfa635/cfa635.go
@@ -0,0 +1,226 @@
+// Copyright 2022 Benjamin Barenblat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Package cfa635 interfaces with Crystalfontz CFA635-xxx-KU LCD modules.
+//
+// The Connect function adopts an existing serial port:
+//
+// s, err := serial.OpenPort(&serial.Config{Name: "/dev/ttyUSB0", Baud: 115200})
+// if err != nil {
+// panic(err)
+// }
+// m := cfa635.Connect(s)
+// defer m.Close()
+//
+// Having connected, you can then issue commands to the module:
+//
+// msg, _, err := transform.String(cfa635.NewEncoder(), "Hello, world!")
+// if err != nil {
+// panic(err)
+// }
+// if err := m.Put(0, 0, []byte(msg)); err != nil {
+// panic(err)
+// }
+//
+// You can also use the State type and Update function to send the optimal
+// sequence of commands to the module to transform its state:
+//
+// if err := m.Reset(); err != nil {
+// panic(err)
+// }
+// s := new(cfa635.State)
+// t := new(cfa635.State)
+// copy(t.LCD[0][0:19], msg)
+// if err := cfa635.Update(m, s, t); err != nil {
+// panic(err)
+// }
+// s = t
+package cfa635
+
+import (
+ "bufio"
+ "errors"
+ "io"
+ "reflect"
+ "sync"
+ "time"
+)
+
+var (
+ ErrPayloadTooLarge = errors.New("payload too large")
+ ErrCGRAM = errors.New("CGRAM index out of range")
+ ErrSprite = errors.New("invalid sprite")
+ ErrPosition = errors.New("position out of range")
+ ErrBacklight = errors.New("backlight brightness out of range")
+ ErrLEDIndex = errors.New("LED index out of range")
+ ErrLEDDuty = errors.New("LED duty cycle out of range")
+
+ ErrTimeout = errors.New("timed out")
+
+ ErrFailed = errors.New("command failed")
+)
+
+// Module is a handle to a CFA635 module.
+type Module struct {
+ w io.WriteCloser
+ reports chan any // Messages initiated by the CFA635
+ responses chan []byte // Responses to messages initiated by the host
+
+ // Ensures that only one request is in flight to the CFA635 at once
+ mu sync.Mutex
+}
+
+// Connect constructs a Module from a serial connection to a CFA635.
+func Connect(cfa635 io.ReadWriteCloser) *Module {
+ m := Module{w: cfa635, reports: make(chan any), responses: make(chan []byte)}
+
+ bytes := make(chan byte)
+ go buffer(bufio.NewReader(cfa635), bytes)
+ packets := make(chan []byte)
+ go decode(bytes, packets)
+ go route(packets, m.reports, m.responses)
+
+ return &m
+}
+
+// Close releases the CFA635 and closes the underlying connection.
+func (m *Module) Close() { m.w.Close() }
+
+// ReadReport blocks until the CFA635 sends a report to the host and then
+// returns it. The returned report will be a *KeyActivity, *FanSpeed, or
+// *Temperature.
+func (m *Module) ReadReport() any { return <-m.reports }
+
+// RawCommand sends an arbitrary command (request and payload) to the CFA635,
+// returning the response (and payload) or error.
+func (m *Module) RawCommand(req byte, reqP []byte) (resp byte, respP []byte, err error) {
+ p := []byte{req, byte(len(reqP))}
+ p = append(p, reqP...)
+ p = pushCRC(p)
+
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ if _, err := m.w.Write(p); err != nil {
+ return 0, nil, err
+ }
+
+ var q []byte
+ select {
+ case q = <-m.responses:
+ break
+ case <-time.After(timeout):
+ return 0, nil, ErrTimeout
+ }
+
+ if len(q) < 2 || len(q) != 2+int(q[1]) {
+ return 0, nil, ErrFailed
+ }
+
+ return q[0], q[2:], nil
+}
+
+func (m *Module) simple(req byte, reqP []byte, wantC byte, wantP []byte) error {
+ gotC, gotP, err := m.RawCommand(req, reqP)
+ if err != nil {
+ return err
+ }
+
+ // DeepEqual treats nil and empty slices as non-equal, so check for
+ // emptiness explicitly.
+ if gotC != wantC || (len(wantP) != 0 || len(gotP) != 0) && !reflect.DeepEqual(gotP, wantP) {
+ return ErrFailed
+ }
+ return nil
+}
+
+// Ping pings the CFA635 with a payload of up to 16 bytes.
+func (m *Module) Ping(payload []byte) error {
+ if len(payload) > 16 {
+ return ErrPayloadTooLarge
+ }
+ return m.simple(0x00, payload, 0x40, payload)
+}
+
+// Clear clears the CFA635 LCD. After Clear returns successfully, all LCD cells
+// hold 0x20 (space).
+func (m *Module) Clear() error { return m.simple(0x06, nil, 0x46, nil) }
+
+// SetCharacter sets a sprite (six columns by eight rows) in character generator
+// RAM. The index of the sprite must be between 0 and 7, inclusive.
+//
+// The sprite itself is specified as an 8-element byte array, each byte
+// representing a row. In each byte, the upper two bits must be 0; the lower six
+// determine the six pixels in the row, with 1 bits corresponding to active
+// pixels and 0 bits corresponding to inactive ones.
+func (m *Module) SetCharacter(i int, data *[8]byte) error {
+ if i < 0 || i > 7 {
+ return ErrCGRAM
+ }
+ for _, b := range data {
+ if b&0b11_000000 != 0 {
+ return ErrSprite
+ }
+ }
+
+ payload := []byte{byte(i)}
+ payload = append(payload, data[:]...)
+ return m.simple(0x09, payload, 0x49, nil)
+}
+
+// SetBacklight controls the LEDs backing the LCD and keypad. Each LED value can
+// range from 0 to 100, inclusive, with 0 turning off the light and 100 turning
+// it on to its maximum brightness.
+func (m *Module) SetBacklight(lcd, keypad int) error {
+ if lcd < 0 || lcd > 100 || keypad < 0 || keypad > 100 {
+ return ErrBacklight
+ }
+
+ return m.simple(0x0e, []byte{byte(lcd), byte(keypad)}, 0x4e, nil)
+}
+
+// Put writes data to the LCD at a row and column. No wrapping occurs; if the
+// data are too large, they are truncated. Data are interpreted in the CFA635
+// character set; see NewEncoder.
+func (m *Module) Put(col, row int, data []byte) error {
+ if col < 0 || col >= 20 || row < 0 || row >= 4 {
+ return ErrPosition
+ }
+ width := 20 - col
+ if len(data) > width {
+ data = data[:width]
+ }
+
+ payload := []byte{byte(col), byte(row)}
+ payload = append(payload, data...)
+ return m.simple(0x1f, payload, 0x5f, nil)
+}
+
+// SetLED controls the four red/green LEDs to the left of the LCD. The LEDs are
+// numbered 0 through 3 from top to bottom; for each, the red and green
+// components can be set separately to a value from 0 (off) to 100 (full duty
+// cycle).
+func (m *Module) SetLED(led int, green bool, duty int) error {
+ if led < 0 || led > 3 {
+ return ErrLEDIndex
+ }
+ if duty < 0 || duty > 100 {
+ return ErrLEDDuty
+ }
+
+ i := byte(11 - 2*led)
+ if !green {
+ i++
+ }
+ return m.simple(0x22, []byte{i, byte(duty)}, 0x62, nil)
+}