summaryrefslogtreecommitdiff
path: root/cfa635/cfa635.go
blob: 296ab21437b12938dfd6df8b7dd43ea546adff90 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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)
}