diff options
Diffstat (limited to 'cfa635/cfa635.go')
-rw-r--r-- | cfa635/cfa635.go | 226 |
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) +} |