mirror of https://github.com/periph/devices
Initial commit of pio.
This was extracted from github.com/maruel/dlibox/go/pio @ f51102b. This code was written by Marc-Antoine Ruel.pull/1/head
commit
eb384b9d0c
@ -0,0 +1,6 @@
|
||||
# This is the list of pio authors for copyright purposes.
|
||||
#
|
||||
# This does not necessarily list everyone who has contributed code, since in
|
||||
# some cases, their employer may be the copyright holder. To see the full list
|
||||
# of contributors, see the revision history in source control.
|
||||
Google Inc.
|
||||
@ -0,0 +1,29 @@
|
||||
# This is the official list of people who can contribute
|
||||
# (and typically have contributed) code to the pio repository.
|
||||
# The AUTHORS file lists the copyright holders; this file
|
||||
# lists people. For example, Google employees are listed here
|
||||
# but not in AUTHORS, because Google holds the copyright.
|
||||
#
|
||||
# Names should be added to this file only after verifying that
|
||||
# the individual or the individual's organization has agreed to
|
||||
# the appropriate Contributor License Agreement, found here:
|
||||
#
|
||||
# https://cla.developers.google.com/
|
||||
#
|
||||
# When adding J Random Contributor's name to this file,
|
||||
# either J's name or J's organization's name should be
|
||||
# added to the AUTHORS file, depending on whether the
|
||||
# individual or corporate CLA was used.
|
||||
|
||||
# Names should be added to this file like so:
|
||||
# Individual's name <submission email address>
|
||||
# Individual's name <submission email address> <email2> <emailN>
|
||||
#
|
||||
# An entry with multiple email addresses specifies that the
|
||||
# first address should be used in the submit logs and
|
||||
# that the other addresses should be recognized as the
|
||||
# same person when interacting with Gerrit.
|
||||
|
||||
# Please keep the list sorted.
|
||||
|
||||
Marc-Antoine Ruel <maruel@chromium.org> <maruel@gmail.com>
|
||||
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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
|
||||
|
||||
http://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.
|
||||
@ -0,0 +1,139 @@
|
||||
# pio - Peripherals I/O in Go
|
||||
|
||||
* [doc/users/](doc/users/) for ready-to-use tools.
|
||||
* [doc/apps/](doc/apps/) to use `pio` as a library. The complete API
|
||||
documentation, including examples, is at
|
||||
[](https://godoc.org/github.com/google/pio).
|
||||
* [doc/drivers/](doc/drivers/) to expand the list of supported hardware.
|
||||
|
||||
|
||||
## Users
|
||||
|
||||
pio includes [many ready-to-use tools](cmd/)! See [doc/users/](doc/users/) for
|
||||
more info on configuring the host and using the included tools.
|
||||
|
||||
```bash
|
||||
go get github.com/google/pio/cmd/...
|
||||
pio-info
|
||||
headers-list
|
||||
```
|
||||
|
||||
|
||||
## Application developpers
|
||||
|
||||
For [application developpers](doc/apps/), `pio` provides OS-independent bus
|
||||
interfacing. The following gets the current temperature, barometric pressure and
|
||||
relative humidity using a bme280:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/google/pio/devices"
|
||||
"github.com/google/pio/devices/bme280"
|
||||
"github.com/google/pio/host"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load all the drivers:
|
||||
if _, err := host.Init(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Open a handle to the first available I²C bus. It could be a via FT232H
|
||||
// over USB or an I²C bus exposed on the host's headers, it doesn't matter.
|
||||
bus, err := i2c.New(-1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer bus.Close()
|
||||
|
||||
// Open a handle to a bme280 connected on the I²C bus using default settings:
|
||||
dev, err := bme280.NewI2C(bus, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer dev.Close()
|
||||
|
||||
// Read temperature from the sensor:
|
||||
var env devices.Environment
|
||||
if err = dev.Sense(&env); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%8s %10s %9s\n", env.Temperature, env.Pressure, env.Humidity)
|
||||
}
|
||||
```
|
||||
|
||||
See more examples at [doc/apps/SAMPLES.md](doc/apps/SAMPLES.md)!
|
||||
|
||||
|
||||
## Contributions
|
||||
|
||||
`pio` provides an extensible driver registry and common bus interfaces which are
|
||||
explained in more details at [doc/drivers/](doc/drivers/). `pio` is designed to
|
||||
work well with drivers living in external repositories so you are not _required_
|
||||
to fork to load drivers for your platform.
|
||||
|
||||
We gladly accept contributions from device driver developpers via GitHub pull
|
||||
requests, as long as the author has signed the Google Contributor License.
|
||||
Please see [doc/drivers/CONTRIBUTING.md](doc/drivers/CONTRIBUTING.md) for more
|
||||
details.
|
||||
|
||||
|
||||
## Philosophy
|
||||
|
||||
1. Optimize for simplicity, correctness and usability in that order.
|
||||
* e.g. everything, interfaces and structs, uses strict typing, there's no
|
||||
`interface{}` in sight.
|
||||
2. OS agnostic. Clear separation of interfaces in [conn/](conn/),
|
||||
enablers in [host/](host) and device drivers in [devices/](devices/).
|
||||
* e.g. no devfs or sysfs path in sight.
|
||||
* e.g. conditional compilation enables only the relevant drivers to be loaded
|
||||
on each platform.
|
||||
3. ... yet doesn't get in the way of platform specific code.
|
||||
* e.g. A user can use statically typed global variables
|
||||
[rpi.P1_3](https://godoc.org/github.com/google/pio/host/rpi#pkg-variables),
|
||||
[bcm283x.GPIO2](https://godoc.org/github.com/google/pio/host/bcm283x#Pin)
|
||||
or
|
||||
[bcm283x.I2C1_SDA](https://godoc.org/github.com/google/pio/host/bcm283x#pkg-variables)
|
||||
to refer to the exact same pin when I²C bus #1 is enabled on a Raspberry
|
||||
Pi.
|
||||
3. The user can chose to optimize for performance instead of usability.
|
||||
* e.g.
|
||||
[apa102.Dev](https://godoc.org/github.com/google/pio/devices/apa102#Dev)
|
||||
exposes both high level
|
||||
[draw.Image](https://golang.org/pkg/image/draw/#Image) to draw an image and
|
||||
low level [io.Writer](https://golang.org/pkg/io/#Writer) to write raw RGB
|
||||
24 bits pixels. The user chooses.
|
||||
4. Use a divide and conquer approach. Each component has exactly one
|
||||
responsibility.
|
||||
* e.g. instead of having a driver per "platform", there's a driver per
|
||||
"component": one for the CPU, one for the board headers, one for each
|
||||
buses and sensors, etc.
|
||||
5. Extensible via a [driver
|
||||
registry](https://godoc.org/github.com/google/pio#Register).
|
||||
* e.g. a user can inject a custom driver to expose more pins, headers, etc.
|
||||
An USB device (like an FT232H) can expose headers _in addition_ to the
|
||||
headers found on the host.
|
||||
6. The drivers must use the fastest possible implementation.
|
||||
* e.g. both
|
||||
[allwinner](https://godoc.org/github.com/google/pio/host/allwinner)
|
||||
and
|
||||
[bcm283x](https://godoc.org/github.com/google/pio/host/bcm283x)
|
||||
leverage sysfs gpio to expose interrupt driven edge detection, yet use
|
||||
memory mapped GPIO registers to single-cycle reads and writes.
|
||||
|
||||
|
||||
## Authors
|
||||
|
||||
The main author is [Marc-Antoine Ruel](https://github.com/maruel). The full list
|
||||
is in [AUTHORS](AUTHORS) and [CONTRIBUTORS](CONTRIBUTORS).
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is not an official Google product (experimental or otherwise), it
|
||||
is just code that happens to be owned by Google.
|
||||
@ -0,0 +1,306 @@
|
||||
// Copyright 2016 Google Inc. 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 apa102
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/google/pio/conn/spi"
|
||||
"github.com/google/pio/devices"
|
||||
"github.com/maruel/temperature"
|
||||
)
|
||||
|
||||
// maxOut is the maximum intensity of each channel on a APA102 LED.
|
||||
const maxOut = 0x1EE1
|
||||
|
||||
// ramp converts input from [0, 0xFF] as intensity to lightness on a scale of
|
||||
// [0, maxOut] or other desired range [0, max].
|
||||
//
|
||||
// It tries to use the same curve independent of the scale used. max can be
|
||||
// changed to change the color temperature or to limit power dissipation.
|
||||
//
|
||||
// It's the reverse of lightness; https://en.wikipedia.org/wiki/Lightness
|
||||
func ramp(l uint8, max uint16) uint16 {
|
||||
if l == 0 {
|
||||
// Make sure black is black.
|
||||
return 0
|
||||
}
|
||||
// linearCutOff defines the linear section of the curve. Inputs between
|
||||
// [0, linearCutOff] are mapped linearly to the output. It is 1% of maximum
|
||||
// output.
|
||||
linearCutOff := uint32((max + 50) / 100)
|
||||
l32 := uint32(l)
|
||||
if l32 < linearCutOff {
|
||||
return uint16(l32)
|
||||
}
|
||||
|
||||
// Maps [linearCutOff, 255] to use [linearCutOff*max/255, max] using a x^3
|
||||
// ramp.
|
||||
// Realign input to [0, 255-linearCutOff]. It now maps to
|
||||
// [0, max-linearCutOff*max/255].
|
||||
//const inRange = 255
|
||||
l32 -= linearCutOff
|
||||
inRange := 255 - linearCutOff
|
||||
outRange := uint32(max) - linearCutOff
|
||||
offset := inRange >> 1
|
||||
y := (l32*l32*l32 + offset) / inRange
|
||||
return uint16((y*outRange+(offset*offset))/inRange/inRange + linearCutOff)
|
||||
}
|
||||
|
||||
// lut is a lookup table that initializes itself on the fly.
|
||||
type lut struct {
|
||||
intensity uint8 // Set an intensity between 0 (off) and 255 (full brightness).
|
||||
temperature uint16 // In Kelvin.
|
||||
r [256]uint16
|
||||
g [256]uint16
|
||||
b [256]uint16
|
||||
}
|
||||
|
||||
func (l *lut) init(i uint8, t uint16) {
|
||||
if i != l.intensity || t != l.temperature {
|
||||
l.intensity = i
|
||||
l.temperature = t
|
||||
tr, tg, tb := temperature.ToRGB(l.temperature)
|
||||
maxR := uint16((uint32(maxOut)*uint32(l.intensity)*uint32(tr) + 127*127) / 65025)
|
||||
maxG := uint16((uint32(maxOut)*uint32(l.intensity)*uint32(tg) + 127*127) / 65025)
|
||||
maxB := uint16((uint32(maxOut)*uint32(l.intensity)*uint32(tb) + 127*127) / 65025)
|
||||
for i := range l.r {
|
||||
l.r[i] = ramp(uint8(i), maxR)
|
||||
}
|
||||
if maxG == maxR {
|
||||
copy(l.g[:], l.r[:])
|
||||
} else {
|
||||
for i := range l.g {
|
||||
l.g[i] = ramp(uint8(i), maxG)
|
||||
}
|
||||
}
|
||||
if maxB == maxR {
|
||||
copy(l.b[:], l.r[:])
|
||||
} else if maxB == maxG {
|
||||
copy(l.b[:], l.g[:])
|
||||
} else {
|
||||
for i := range l.b {
|
||||
l.b[i] = ramp(uint8(i), maxB)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// raster serializes converts a buffer of RGB bytes to the APA102 SPI format.
|
||||
//
|
||||
// It is expected to be given the part where pixels are, not the header nor
|
||||
// footer.
|
||||
//
|
||||
// dst is in APA102 SPI 32 bits word format. src is in RGB 24 bits word format.
|
||||
// maxR, maxG and maxB are the maximum light intensity to use per channel.
|
||||
func (l *lut) raster(dst []byte, src []byte) {
|
||||
// Whichever is the shortest.
|
||||
length := len(src) / 3
|
||||
if o := len(dst) / 4; o < length {
|
||||
length = o
|
||||
}
|
||||
for i := 0; i < length; i++ {
|
||||
// Converts a color into the 4 bytes needed to control an APA-102 LED.
|
||||
//
|
||||
// The response as seen by the human eye is very non-linear. The APA-102
|
||||
// provides an overall brightness PWM but it is relatively slower and
|
||||
// results in human visible flicker. On the other hand the minimal color
|
||||
// (1/255) is still too intense at full brightness, so for very dark color,
|
||||
// it is worth using the overall brightness PWM. The goal is to use
|
||||
// brightness!=31 as little as possible.
|
||||
//
|
||||
// Global brightness frequency is 580Hz and color frequency at 19.2kHz.
|
||||
// https://cpldcpu.wordpress.com/2014/08/27/apa102/
|
||||
// Both are multiplicative, so brightness@50% and color@50% means an
|
||||
// effective 25% duty cycle but it is not properly distributed, which is
|
||||
// the main problem.
|
||||
//
|
||||
// It is unclear to me if brightness is exactly in 1/31 increment as I don't
|
||||
// have an oscilloscope to confirm. Same for color in 1/255 increment.
|
||||
// TODO(maruel): I have one now!
|
||||
//
|
||||
// Each channel duty cycle ramps from 100% to 1/(31*255) == 1/7905.
|
||||
//
|
||||
// Computes brighness, blue, green, red.
|
||||
j := 3 * i
|
||||
r := l.r[src[j]]
|
||||
g := l.g[src[j+1]]
|
||||
b := l.b[src[j+2]]
|
||||
m := r | g | b
|
||||
j += i
|
||||
if m <= 1023 {
|
||||
if m <= 255 {
|
||||
dst[j], dst[j+1], dst[j+2], dst[j+3] = byte(0xE0+1), byte(b), byte(g), byte(r)
|
||||
} else if m <= 511 {
|
||||
dst[j], dst[j+1], dst[j+2], dst[j+3] = byte(0xE0+2), byte(b>>1), byte(g>>1), byte(r>>1)
|
||||
} else {
|
||||
dst[j], dst[j+1], dst[j+2], dst[j+3] = byte(0xE0+4), byte((b+2)>>2), byte((g+2)>>2), byte((r+2)>>2)
|
||||
}
|
||||
} else {
|
||||
// In this case we need to use a ramp of 255-1 even for lower colors.
|
||||
dst[j], dst[j+1], dst[j+2], dst[j+3] = byte(0xE0+31), byte((b+15)/31), byte((g+15)/31), byte((r+15)/31)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rasterImg is the generic version of raster.
|
||||
func (l *lut) rasterImg(dst []byte, r image.Rectangle, src image.Image, srcR image.Rectangle) {
|
||||
// Render directly into the buffer for maximum performance and to keep
|
||||
// untouched sections intact.
|
||||
deltaX4 := 4 * (r.Min.X - srcR.Min.X)
|
||||
if img, ok := src.(*image.NRGBA); ok {
|
||||
// Fast path for image.NRGBA.
|
||||
pix := img.Pix[srcR.Min.Y*img.Stride:]
|
||||
for sX := srcR.Min.X; sX < srcR.Max.X; sX++ {
|
||||
sX4 := 4 * sX
|
||||
r := l.r[pix[sX4]]
|
||||
g := l.g[pix[sX4+1]]
|
||||
b := l.b[pix[sX4+2]]
|
||||
m := r | g | b
|
||||
rX := sX4 + deltaX4
|
||||
if m <= 1023 {
|
||||
if m <= 255 {
|
||||
dst[rX], dst[rX+1], dst[rX+2], dst[rX+3] = byte(0xE0+1), byte(b), byte(g), byte(r)
|
||||
} else if m <= 511 {
|
||||
dst[rX], dst[rX+1], dst[rX+2], dst[rX+3] = byte(0xE0+2), byte(b>>1), byte(g>>1), byte(r>>1)
|
||||
} else {
|
||||
dst[rX], dst[rX+1], dst[rX+2], dst[rX+3] = byte(0xE0+4), byte((b+2)>>2), byte((g+2)>>2), byte((r+2)>>2)
|
||||
}
|
||||
} else {
|
||||
// In this case we need to use a ramp of 255-1 even for lower colors.
|
||||
dst[rX], dst[rX+1], dst[rX+2], dst[rX+3] = byte(0xE0+31), byte((b+15)/31), byte((g+15)/31), byte((r+15)/31)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Generic version.
|
||||
for sX := srcR.Min.X; sX < srcR.Max.X; sX++ {
|
||||
r16, g16, b16, _ := src.At(sX, srcR.Min.Y).RGBA()
|
||||
r := l.r[byte(r16>>8)]
|
||||
g := l.g[byte(g16>>8)]
|
||||
b := l.b[byte(b16>>8)]
|
||||
m := r | g | b
|
||||
rX := sX*4 + deltaX4
|
||||
if m <= 1023 {
|
||||
if m <= 255 {
|
||||
dst[rX], dst[rX+1], dst[rX+2], dst[rX+3] = byte(0xE0+1), byte(b), byte(g), byte(r)
|
||||
} else if m <= 511 {
|
||||
dst[rX], dst[rX+1], dst[rX+2], dst[rX+3] = byte(0xE0+2), byte(b>>1), byte(g>>1), byte(r>>1)
|
||||
} else {
|
||||
dst[rX], dst[rX+1], dst[rX+2], dst[rX+3] = byte(0xE0+4), byte((b+2)>>2), byte((g+2)>>2), byte((r+2)>>2)
|
||||
}
|
||||
} else {
|
||||
// In this case we need to use a ramp of 255-1 even for lower colors.
|
||||
dst[rX], dst[rX+1], dst[rX+2], dst[rX+3] = byte(0xE0+31), byte((b+15)/31), byte((g+15)/31), byte((r+15)/31)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToRGB converts a slice of color.NRGBA to a byte stream of RGB pixels.
|
||||
//
|
||||
// Ignores alpha.
|
||||
func ToRGB(p []color.NRGBA) []byte {
|
||||
b := make([]byte, 0, len(p)*3)
|
||||
for _, c := range p {
|
||||
b = append(b, c.R, c.G, c.B)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Dev represents a strip of APA-102 LEDs as a strip connected over a SPI bus.
|
||||
// It accepts a stream of raw RGB pixels and converts it to the full dynamic
|
||||
// range as supported by APA102 protocol (nearly 8000:1 contrast ratio).
|
||||
//
|
||||
// Includes intensity and temperature correction.
|
||||
type Dev struct {
|
||||
Intensity uint8 // Set an intensity between 0 (off) and 255 (full brightness).
|
||||
Temperature uint16 // In Kelvin.
|
||||
s spi.Conn
|
||||
l lut // Updated at each .Write() call.
|
||||
numLights int
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// ColorModel implements devices.Display. There's no surprise, it is
|
||||
// color.NRGBAModel.
|
||||
func (d *Dev) ColorModel() color.Model {
|
||||
return color.NRGBAModel
|
||||
}
|
||||
|
||||
// Bounds implements devices.Display. Min is guaranteed to be {0, 0}.
|
||||
func (d *Dev) Bounds() image.Rectangle {
|
||||
return image.Rectangle{Max: image.Point{X: d.numLights, Y: 1}}
|
||||
}
|
||||
|
||||
// Draw implements devices.Display.
|
||||
//
|
||||
// Using something else than image.NRGBA is 10x slower. When using image.NRGBA,
|
||||
// the alpha channel is ignored.
|
||||
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
|
||||
r = r.Intersect(d.Bounds())
|
||||
srcR := src.Bounds()
|
||||
srcR.Min = srcR.Min.Add(sp)
|
||||
if dX := r.Dx(); dX < srcR.Dx() {
|
||||
srcR.Max.X = srcR.Min.X + dX
|
||||
}
|
||||
if dY := r.Dy(); dY < srcR.Dy() {
|
||||
srcR.Max.Y = srcR.Min.Y + dY
|
||||
}
|
||||
d.l.init(d.Intensity, d.Temperature)
|
||||
d.l.rasterImg(d.buf[4:4+4*d.numLights], r, src, srcR)
|
||||
_, _ = d.s.Write(d.buf)
|
||||
}
|
||||
|
||||
// Write accepts a stream of raw RGB pixels and sends it as APA102 encoded
|
||||
// stream.
|
||||
func (d *Dev) Write(pixels []byte) (int, error) {
|
||||
if len(pixels)%3 != 0 {
|
||||
return 0, errLength
|
||||
}
|
||||
d.l.init(d.Intensity, d.Temperature)
|
||||
d.l.raster(d.buf[4:4+4*d.numLights], pixels)
|
||||
_, err := d.s.Write(d.buf)
|
||||
return len(pixels), err
|
||||
}
|
||||
|
||||
// New returns a strip that communicates over SPI to APA102 LEDs.
|
||||
//
|
||||
// The SPI bus speed should be high, at least in the Mhz range, as
|
||||
// there's 32 bits sent per LED, creating a staggered effect. See
|
||||
// https://cpldcpu.wordpress.com/2014/11/30/understanding-the-apa102-superled/
|
||||
//
|
||||
// Temperature is in °Kelvin and a reasonable default value is 6500°K.
|
||||
//
|
||||
// As per APA102-C spec, the chip's max refresh rate is 400hz.
|
||||
// https://en.wikipedia.org/wiki/Flicker_fusion_threshold is a recommended
|
||||
// reading.
|
||||
func New(s spi.Conn, numLights int, intensity uint8, temperature uint16) (*Dev, error) {
|
||||
if err := s.Configure(spi.Mode3, 8); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// End frames are needed to be able to push enough SPI clock signals due to
|
||||
// internal half-delay of data signal from each individual LED. See
|
||||
// https://cpldcpu.wordpress.com/2014/11/30/understanding-the-apa102-superled/
|
||||
buf := make([]byte, 4*(numLights+1)+numLights/2/8+1)
|
||||
tail := buf[4+4*numLights:]
|
||||
for i := range tail {
|
||||
tail[i] = 0xFF
|
||||
}
|
||||
return &Dev{
|
||||
Intensity: intensity,
|
||||
Temperature: temperature,
|
||||
s: s,
|
||||
numLights: numLights,
|
||||
buf: buf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
var errLength = errors.New("invalid RGB stream length")
|
||||
|
||||
var _ devices.Display = &Dev{}
|
||||
@ -0,0 +1,627 @@
|
||||
// Copyright 2016 Google Inc. 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 apa102
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/google/pio/conn/spi"
|
||||
"github.com/google/pio/conn/spi/spitest"
|
||||
)
|
||||
|
||||
func TestRamp(t *testing.T) {
|
||||
// Tests a few known values.
|
||||
data := []struct {
|
||||
input uint8
|
||||
expected uint16
|
||||
}{
|
||||
{0x00, 0x0000},
|
||||
{0x01, 0x0001},
|
||||
{0x02, 0x0002},
|
||||
{0x03, 0x0003},
|
||||
{0x04, 0x0004},
|
||||
{0x05, 0x0005},
|
||||
{0x06, 0x0006},
|
||||
{0x07, 0x0007},
|
||||
{0x08, 0x0008},
|
||||
{0x09, 0x0009},
|
||||
{0x0A, 0x000A},
|
||||
{0x0B, 0x000B},
|
||||
{0x0C, 0x000C},
|
||||
{0x0D, 0x000D},
|
||||
{0x0E, 0x000E},
|
||||
{0x0F, 0x000F},
|
||||
{0x10, 0x0010},
|
||||
{0x11, 0x0011},
|
||||
{0x12, 0x0012},
|
||||
{0x13, 0x0013},
|
||||
{0x14, 0x0014},
|
||||
{0x15, 0x0015},
|
||||
{0x16, 0x0016},
|
||||
{0x17, 0x0017},
|
||||
{0x18, 0x0018},
|
||||
{0x19, 0x0019},
|
||||
{0x1A, 0x001A},
|
||||
{0x1B, 0x001B},
|
||||
{0x1C, 0x001C},
|
||||
{0x1D, 0x001D},
|
||||
{0x1E, 0x001E},
|
||||
{0x1F, 0x001F},
|
||||
{0x20, 0x0020},
|
||||
{0x21, 0x0021},
|
||||
{0x22, 0x0022},
|
||||
{0x23, 0x0023},
|
||||
{0x24, 0x0024},
|
||||
{0x25, 0x0025},
|
||||
{0x26, 0x0026},
|
||||
{0x27, 0x0027},
|
||||
{0x28, 0x0028},
|
||||
{0x29, 0x0029},
|
||||
{0x2A, 0x002A},
|
||||
{0x2B, 0x002B},
|
||||
{0x2C, 0x002C},
|
||||
{0x2D, 0x002D},
|
||||
{0x2E, 0x002E},
|
||||
{0x2F, 0x002F},
|
||||
{0x30, 0x0030},
|
||||
{0x31, 0x0031},
|
||||
{0x32, 0x0032},
|
||||
{0x33, 0x0033},
|
||||
{0x34, 0x0034},
|
||||
{0x35, 0x0035},
|
||||
{0x36, 0x0036},
|
||||
{0x37, 0x0037},
|
||||
{0x38, 0x0038},
|
||||
{0x39, 0x0039},
|
||||
{0x3A, 0x003A},
|
||||
{0x3B, 0x003B},
|
||||
{0x3C, 0x003C},
|
||||
{0x3D, 0x003D},
|
||||
{0x3E, 0x003E},
|
||||
{0x3F, 0x003F},
|
||||
{0x40, 0x0040},
|
||||
{0x41, 0x0041},
|
||||
{0x42, 0x0042},
|
||||
{0x43, 0x0043},
|
||||
{0x44, 0x0044},
|
||||
{0x45, 0x0045},
|
||||
{0x46, 0x0046},
|
||||
{0x47, 0x0047},
|
||||
{0x48, 0x0048},
|
||||
{0x49, 0x0049},
|
||||
{0x4A, 0x004A},
|
||||
{0x4B, 0x004B},
|
||||
{0x4C, 0x004C},
|
||||
{0x4D, 0x004D},
|
||||
{0x4E, 0x004E},
|
||||
{0x4F, 0x004F},
|
||||
{0x50, 0x004F},
|
||||
{0x51, 0x004F},
|
||||
{0x52, 0x004F},
|
||||
{0x53, 0x004F},
|
||||
{0x54, 0x004F},
|
||||
{0x55, 0x004F},
|
||||
{0x56, 0x004F},
|
||||
{0x57, 0x0050},
|
||||
{0x58, 0x0050},
|
||||
{0x59, 0x0050},
|
||||
{0x5A, 0x0051},
|
||||
{0x5B, 0x0051},
|
||||
{0x5C, 0x0052},
|
||||
{0x5D, 0x0053},
|
||||
{0x5E, 0x0054},
|
||||
{0x5F, 0x0055},
|
||||
{0x60, 0x0056},
|
||||
{0x61, 0x0057},
|
||||
{0x62, 0x0059},
|
||||
{0x63, 0x005A},
|
||||
{0x64, 0x005C},
|
||||
{0x65, 0x005E},
|
||||
{0x66, 0x0060},
|
||||
{0x67, 0x0063},
|
||||
{0x68, 0x0065},
|
||||
{0x69, 0x0068},
|
||||
{0x6A, 0x006B},
|
||||
{0x6B, 0x006E},
|
||||
{0x6C, 0x0072},
|
||||
{0x6D, 0x0075},
|
||||
{0x6E, 0x0079},
|
||||
{0x6F, 0x007E},
|
||||
{0x70, 0x0082},
|
||||
{0x71, 0x0087},
|
||||
{0x72, 0x008C},
|
||||
{0x73, 0x0092},
|
||||
{0x74, 0x0098},
|
||||
{0x75, 0x009E},
|
||||
{0x76, 0x00A4},
|
||||
{0x77, 0x00AB},
|
||||
{0x78, 0x00B2},
|
||||
{0x79, 0x00B9},
|
||||
{0x7A, 0x00C1},
|
||||
{0x7B, 0x00C9},
|
||||
{0x7C, 0x00D2},
|
||||
{0x7D, 0x00DA},
|
||||
{0x7E, 0x00E4},
|
||||
{0x7F, 0x00ED},
|
||||
{0x80, 0x00F8},
|
||||
{0x81, 0x0102},
|
||||
{0x82, 0x010D},
|
||||
{0x83, 0x0119},
|
||||
{0x84, 0x0124},
|
||||
{0x85, 0x0131},
|
||||
{0x86, 0x013E},
|
||||
{0x87, 0x014B},
|
||||
{0x88, 0x0159},
|
||||
{0x89, 0x0167},
|
||||
{0x8A, 0x0176},
|
||||
{0x8B, 0x0185},
|
||||
{0x8C, 0x0195},
|
||||
{0x8D, 0x01A5},
|
||||
{0x8E, 0x01B6},
|
||||
{0x8F, 0x01C7},
|
||||
{0x90, 0x01D9},
|
||||
{0x91, 0x01EC},
|
||||
{0x92, 0x01FF},
|
||||
{0x93, 0x0212},
|
||||
{0x94, 0x0226},
|
||||
{0x95, 0x023B},
|
||||
{0x96, 0x0251},
|
||||
{0x97, 0x0267},
|
||||
{0x98, 0x027D},
|
||||
{0x99, 0x0294},
|
||||
{0x9A, 0x02AC},
|
||||
{0x9B, 0x02C5},
|
||||
{0x9C, 0x02DE},
|
||||
{0x9D, 0x02F8},
|
||||
{0x9E, 0x0312},
|
||||
{0x9F, 0x032E},
|
||||
{0xA0, 0x034A},
|
||||
{0xA1, 0x0366},
|
||||
{0xA2, 0x0384},
|
||||
{0xA3, 0x03A2},
|
||||
{0xA4, 0x03C0},
|
||||
{0xA5, 0x03E0},
|
||||
{0xA6, 0x0400},
|
||||
{0xA7, 0x0421},
|
||||
{0xA8, 0x0443},
|
||||
{0xA9, 0x0465},
|
||||
{0xAA, 0x0489},
|
||||
{0xAB, 0x04AC},
|
||||
{0xAC, 0x04D1},
|
||||
{0xAD, 0x04F7},
|
||||
{0xAE, 0x051D},
|
||||
{0xAF, 0x0545},
|
||||
{0xB0, 0x056D},
|
||||
{0xB1, 0x0596},
|
||||
{0xB2, 0x05C0},
|
||||
{0xB3, 0x05EA},
|
||||
{0xB4, 0x0616},
|
||||
{0xB5, 0x0642},
|
||||
{0xB6, 0x066F},
|
||||
{0xB7, 0x069D},
|
||||
{0xB8, 0x06CC},
|
||||
{0xB9, 0x06FC},
|
||||
{0xBA, 0x072D},
|
||||
{0xBB, 0x075F},
|
||||
{0xBC, 0x0792},
|
||||
{0xBD, 0x07C6},
|
||||
{0xBE, 0x07FA},
|
||||
{0xBF, 0x0830},
|
||||
{0xC0, 0x0866},
|
||||
{0xC1, 0x089E},
|
||||
{0xC2, 0x08D6},
|
||||
{0xC3, 0x090F},
|
||||
{0xC4, 0x094A},
|
||||
{0xC5, 0x0985},
|
||||
{0xC6, 0x09C2},
|
||||
{0xC7, 0x09FF},
|
||||
{0xC8, 0x0A3E},
|
||||
{0xC9, 0x0A7D},
|
||||
{0xCA, 0x0ABE},
|
||||
{0xCB, 0x0B00},
|
||||
{0xCC, 0x0B42},
|
||||
{0xCD, 0x0B86},
|
||||
{0xCE, 0x0BCB},
|
||||
{0xCF, 0x0C11},
|
||||
{0xD0, 0x0C58},
|
||||
{0xD1, 0x0CA1},
|
||||
{0xD2, 0x0CEA},
|
||||
{0xD3, 0x0D34},
|
||||
{0xD4, 0x0D80},
|
||||
{0xD5, 0x0DCD},
|
||||
{0xD6, 0x0E1B},
|
||||
{0xD7, 0x0E6A},
|
||||
{0xD8, 0x0EBA},
|
||||
{0xD9, 0x0F0B},
|
||||
{0xDA, 0x0F5E},
|
||||
{0xDB, 0x0FB2},
|
||||
{0xDC, 0x1007},
|
||||
{0xDD, 0x105D},
|
||||
{0xDE, 0x10B4},
|
||||
{0xDF, 0x110D},
|
||||
{0xE0, 0x1167},
|
||||
{0xE1, 0x11C2},
|
||||
{0xE2, 0x121F},
|
||||
{0xE3, 0x127C},
|
||||
{0xE4, 0x12DB},
|
||||
{0xE5, 0x133C},
|
||||
{0xE6, 0x139D},
|
||||
{0xE7, 0x1400},
|
||||
{0xE8, 0x1464},
|
||||
{0xE9, 0x14CA},
|
||||
{0xEA, 0x1530},
|
||||
{0xEB, 0x1599},
|
||||
{0xEC, 0x1602},
|
||||
{0xED, 0x166D},
|
||||
{0xEE, 0x16D9},
|
||||
{0xEF, 0x1747},
|
||||
{0xF0, 0x17B6},
|
||||
{0xF1, 0x1826},
|
||||
{0xF2, 0x1898},
|
||||
{0xF3, 0x190B},
|
||||
{0xF4, 0x197F},
|
||||
{0xF5, 0x19F5},
|
||||
{0xF6, 0x1A6D},
|
||||
{0xF7, 0x1AE5},
|
||||
{0xF8, 0x1B60},
|
||||
{0xF9, 0x1BDB},
|
||||
{0xFA, 0x1C58},
|
||||
{0xFB, 0x1CD7},
|
||||
{0xFC, 0x1D57},
|
||||
{0xFD, 0x1DD9},
|
||||
{0xFE, 0x1E5C},
|
||||
{0xFF, 0x1EE1},
|
||||
}
|
||||
if false {
|
||||
for i := 0; i <= 255; i++ {
|
||||
fmt.Printf("{0x%02X, 0x%04X},\n", i, ramp(uint8(i), maxOut))
|
||||
}
|
||||
}
|
||||
for i, line := range data {
|
||||
if i != int(line.input) || line.expected != ramp(line.input, maxOut) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
if 0x00 != ramp(0x00, 0xFF) {
|
||||
t.Fail()
|
||||
}
|
||||
if 0x21 != ramp(0x7F, 0xFF) {
|
||||
t.Fail()
|
||||
}
|
||||
if 0xFF != ramp(0xFF, 0xFF) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRampMonotonic(t *testing.T) {
|
||||
// Ensures the ramp is 100% monotonically increasing and without bumps.
|
||||
lastValue := uint16(0)
|
||||
lastDelta := uint16(0)
|
||||
for in := uint32(0); in <= 255; in++ {
|
||||
out := ramp(uint8(in), maxOut)
|
||||
if out < lastValue {
|
||||
t.Fatalf("f(%d) = %d; f(%d) = %d", in-1, lastValue, in, out)
|
||||
}
|
||||
if out > maxOut {
|
||||
t.Fatalf("f(%d) = %d", in, out)
|
||||
}
|
||||
|
||||
if out-lastValue+1 < lastDelta {
|
||||
t.Errorf("f(%d)=%d f(%d)=%d f(%d)=%d Deltas: '%d+1 < %d' but should be '>='",
|
||||
in-2, ramp(uint8(in-2), maxOut), in-1, ramp(uint8(in-1), maxOut), in, ramp(uint8(in), maxOut), out-lastValue, lastDelta)
|
||||
}
|
||||
lastDelta = out - lastValue
|
||||
lastValue = out
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevEmpty(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
d, _ := New(spitest.NewRecordRaw(&buf), 0, 255, 6500)
|
||||
if n, err := d.Write([]byte{}); n != 0 || err != nil {
|
||||
t.Fatalf("%d %v", n, err)
|
||||
}
|
||||
if expected := []byte{0x0, 0x0, 0x0, 0x0, 0xFF}; !bytes.Equal(expected, buf.Bytes()) {
|
||||
t.Fatalf("%#v != %#v", expected, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevLen(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
d, _ := New(spitest.NewRecordRaw(&buf), 1, 255, 6500)
|
||||
if n, err := d.Write([]byte{0}); n != 0 || err != errLength {
|
||||
t.Fatalf("%d %v", n, err)
|
||||
}
|
||||
if expected := []byte{}; !bytes.Equal(expected, buf.Bytes()) {
|
||||
t.Fatalf("%#v != %#v", expected, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDev(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
colors := []color.NRGBA{
|
||||
{0xFF, 0xFF, 0xFF, 0x00},
|
||||
{0xFE, 0xFE, 0xFE, 0x00},
|
||||
{0xF0, 0xF0, 0xF0, 0x00},
|
||||
{0x80, 0x80, 0x80, 0x00},
|
||||
{0x80, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x80, 0x00, 0x00},
|
||||
{0x00, 0x00, 0x80, 0x00},
|
||||
{0x00, 0x00, 0x10, 0x00},
|
||||
{0x00, 0x00, 0x01, 0x00},
|
||||
{0x00, 0x00, 0x00, 0x00},
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(&buf), len(colors), 255, 6500)
|
||||
if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil {
|
||||
t.Fatalf("%d %v", n, err)
|
||||
}
|
||||
expected := []byte{
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFB, 0xFB, 0xFB,
|
||||
0xFF, 0xC4, 0xC4, 0xC4,
|
||||
0xE1, 0xF8, 0xF8, 0xF8,
|
||||
0xE1, 0x00, 0x00, 0xF8,
|
||||
0xE1, 0x00, 0xF8, 0x00,
|
||||
0xE1, 0xF8, 0x00, 0x00,
|
||||
0xE1, 0x10, 0x00, 0x00,
|
||||
0xE1, 0x01, 0x00, 0x00,
|
||||
0xE1, 0x00, 0x00, 0x00,
|
||||
0xFF,
|
||||
}
|
||||
if !bytes.Equal(expected, buf.Bytes()) {
|
||||
t.Fatalf("%#v != %#v", expected, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevColo(t *testing.T) {
|
||||
if (&Dev{}).ColorModel() != color.NRGBAModel {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevIntensity(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
colors := []color.NRGBA{
|
||||
{0xFF, 0xFF, 0xFF, 0x00},
|
||||
{0xFE, 0xFE, 0xFE, 0x00},
|
||||
{0xF0, 0xF0, 0xF0, 0x00},
|
||||
{0x80, 0x80, 0x80, 0x00},
|
||||
{0x80, 0x00, 0x00, 0x00},
|
||||
{0x00, 0x80, 0x00, 0x00},
|
||||
{0x00, 0x00, 0x80, 0x00},
|
||||
{0x00, 0x00, 0x10, 0x00},
|
||||
{0x00, 0x00, 0x01, 0x00},
|
||||
{0x00, 0x00, 0x00, 0x00},
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(&buf), len(colors), 127, 6500)
|
||||
if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil {
|
||||
t.Fatalf("%d %v", n, err)
|
||||
}
|
||||
expected := []byte{
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0x7F, 0x7F, 0x7F,
|
||||
0xFF, 0x7D, 0x7D, 0x7D,
|
||||
0xFF, 0x67, 0x67, 0x67,
|
||||
0xE2, 0x9B, 0x9B, 0x9B,
|
||||
0xE2, 0x00, 0x00, 0x9B,
|
||||
0xE2, 0x00, 0x9B, 0x00,
|
||||
0xE2, 0x9B, 0x00, 0x00,
|
||||
0xE1, 0x10, 0x00, 0x00,
|
||||
0xE1, 0x01, 0x00, 0x00,
|
||||
0xE1, 0x00, 0x00, 0x00,
|
||||
0xFF,
|
||||
}
|
||||
if !bytes.Equal(expected, buf.Bytes()) {
|
||||
t.Fatalf("%#v != %#v", expected, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevLong(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
colors := make([]color.NRGBA, 256)
|
||||
d, _ := New(spitest.NewRecordRaw(&buf), len(colors), 255, 6500)
|
||||
if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil {
|
||||
t.Fatalf("%d %v", n, err)
|
||||
}
|
||||
expected := make([]byte, 4*(256+1)+17)
|
||||
for i := 0; i < 256; i++ {
|
||||
expected[4+4*i] = 0xE1
|
||||
}
|
||||
trailer := expected[4*257:]
|
||||
for i := range trailer {
|
||||
trailer[i] = 0xFF
|
||||
}
|
||||
if !bytes.Equal(expected, buf.Bytes()) {
|
||||
t.Fatalf("%#v != %#v", expected, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// Expected output for 3 test cases. Each test case use a completely different
|
||||
// code path so make sure each code path results in the exact same output.
|
||||
var expectedi250t5000 = []byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0xE1, 0x08, 0x04, 0x00, 0xE1, 0x14, 0x10, 0xC, 0xE1,
|
||||
0x20, 0x1C, 0x18, 0xE1, 0x2C, 0x28, 0x24, 0xE1, 0x38, 0x34, 0x30, 0xE1, 0x3F,
|
||||
0x40, 0x3C, 0xE1, 0x43, 0x46, 0x48, 0xE1, 0x54, 0x4C, 0x4E, 0xE1, 0x7B, 0x63,
|
||||
0x56, 0xE1, 0xC1, 0x96, 0x73, 0xE2, 0x97, 0x78, 0x5A, 0xE2, 0xE7, 0xBF, 0x94,
|
||||
0xE4, 0xAA, 0x93, 0x77, 0xE4, 0xF1, 0xD8, 0xB8, 0xFF, 0x2B, 0x27, 0x23, 0xFF,
|
||||
0x39, 0x36, 0x32, 0xFF, 0xFF,
|
||||
}
|
||||
|
||||
func TestDevTemperatureWarm(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
pixels := make([]byte, 16*3)
|
||||
for i := range pixels {
|
||||
// Test all intensity code paths.
|
||||
pixels[i] = uint8(i << 2)
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(&buf), len(pixels)/3, 250, 5000)
|
||||
if n, err := d.Write(pixels); n != len(pixels) || err != nil {
|
||||
t.Fatalf("%d %v", n, err)
|
||||
}
|
||||
if !bytes.Equal(expectedi250t5000, buf.Bytes()) {
|
||||
t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrawNRGBA(t *testing.T) {
|
||||
img := image.NewNRGBA(image.Rect(0, 0, 16, 1))
|
||||
for i := 0; i < 16; i++ {
|
||||
// Test all intensity code paths. Confirm that alpha is ignored.
|
||||
img.Pix[4*i] = uint8((3 * i) << 2)
|
||||
img.Pix[4*i+1] = uint8((3*i + 1) << 2)
|
||||
img.Pix[4*i+2] = uint8((3*i + 2) << 2)
|
||||
img.Pix[4*i+3] = 0
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
d, _ := New(spitest.NewRecordRaw(&buf), 16, 250, 5000)
|
||||
d.Draw(d.Bounds(), img, image.Point{})
|
||||
if !bytes.Equal(expectedi250t5000, buf.Bytes()) {
|
||||
t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrawRGBA(t *testing.T) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 16, 1))
|
||||
for i := 0; i < 16; i++ {
|
||||
// Test all intensity code paths. Alpha is not ignored in this case.
|
||||
img.Pix[4*i] = uint8((3 * i) << 2)
|
||||
img.Pix[4*i+1] = uint8((3*i + 1) << 2)
|
||||
img.Pix[4*i+2] = uint8((3*i + 2) << 2)
|
||||
img.Pix[4*i+3] = 0xFF
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
d, _ := New(spitest.NewRecordRaw(&buf), 16, 250, 5000)
|
||||
d.Draw(d.Bounds(), img, image.Point{})
|
||||
if !bytes.Equal(expectedi250t5000, buf.Bytes()) {
|
||||
t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func Example() {
|
||||
bus, err := spi.New(-1, -1)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open SPI: %v", err)
|
||||
}
|
||||
defer bus.Close()
|
||||
// Opens a strip of 150 lights are 50% intensity with color temperature at
|
||||
// 5000 Kelvin.
|
||||
dev, err := New(bus, 150, 127, 5000)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open apa102: %v", err)
|
||||
}
|
||||
img := image.NewNRGBA(image.Rect(0, 0, dev.Bounds().Dy(), 1))
|
||||
for x := 0; x < img.Rect.Max.X; x++ {
|
||||
img.SetNRGBA(x, 0, color.NRGBA{uint8(x), uint8(255 - x), 0, 255})
|
||||
}
|
||||
dev.Draw(dev.Bounds(), img, image.Point{})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func BenchmarkWriteWhite(b *testing.B) {
|
||||
pixels := make([]byte, 150*3)
|
||||
for i := range pixels {
|
||||
pixels[i] = 0xFF
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 255, 6500)
|
||||
_, _ = d.Write(pixels)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = d.Write(pixels)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteDim(b *testing.B) {
|
||||
pixels := make([]byte, 150*3)
|
||||
for i := range pixels {
|
||||
pixels[i] = 1
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 255, 6500)
|
||||
_, _ = d.Write(pixels)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = d.Write(pixels)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteBlack(b *testing.B) {
|
||||
pixels := make([]byte, 150*3)
|
||||
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 255, 6500)
|
||||
_, _ = d.Write(pixels)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = d.Write(pixels)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteColorful(b *testing.B) {
|
||||
pixels := make([]byte, 150*3)
|
||||
for i := range pixels {
|
||||
pixels[i] = uint8(i) + uint8(i>>8)
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 250, 5000)
|
||||
_, _ = d.Write(pixels)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = d.Write(pixels)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDrawNRGBAColorful(b *testing.B) {
|
||||
// Takes the fast path.
|
||||
img := image.NewNRGBA(image.Rect(0, 0, 150, 1))
|
||||
for i := range img.Pix {
|
||||
img.Pix[i] = uint8(i) + uint8(i>>8)
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), img.Bounds().Max.X, 250, 5000)
|
||||
r := d.Bounds()
|
||||
p := image.Point{}
|
||||
d.Draw(r, img, p)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d.Draw(r, img, p)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDrawRGBAColorful(b *testing.B) {
|
||||
// Takes the slow path.
|
||||
img := image.NewRGBA(image.Rect(0, 0, 256, 1))
|
||||
for i := range img.Pix {
|
||||
img.Pix[i] = uint8(i) + uint8(i>>8)
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), img.Bounds().Max.X, 250, 5000)
|
||||
r := d.Bounds()
|
||||
p := image.Point{}
|
||||
d.Draw(r, img, p)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d.Draw(r, img, p)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteColorfulVariation(b *testing.B) {
|
||||
// Continuously vary the lookup tables.
|
||||
pixels := make([]byte, 256*3)
|
||||
for i := range pixels {
|
||||
pixels[i] = uint8(i) + uint8(i>>8)
|
||||
}
|
||||
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 250, 5000)
|
||||
_, _ = d.Write(pixels)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
d.Intensity = uint8(i)
|
||||
d.Temperature = uint16((3000 + i) & 0x1FFF)
|
||||
_, _ = d.Write(pixels)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
// Copyright 2016 Google Inc. 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 apa102 drives a strip of APA102 LEDs connected on a SPI bus.
|
||||
//
|
||||
// It handles color intensity and temperature correction and uses the full
|
||||
// 8000:1 dynamic range as supported by the device.
|
||||
//
|
||||
// Datasheet
|
||||
//
|
||||
// https://cpldcpu.files.wordpress.com/2014/08/apa-102c-super-led-specifications-2014-en.pdf
|
||||
package apa102
|
||||
@ -0,0 +1,250 @@
|
||||
// Copyright 2016 Google Inc. 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 bme280
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/google/pio/conn/i2c"
|
||||
"github.com/google/pio/conn/i2c/i2ctest"
|
||||
"github.com/google/pio/devices"
|
||||
)
|
||||
|
||||
// Real data extracted from a device.
|
||||
var calib = calibration{
|
||||
t1: 28176,
|
||||
t2: 26220,
|
||||
t3: 350,
|
||||
p1: 38237,
|
||||
p2: -10824,
|
||||
p3: 3024,
|
||||
p4: 7799,
|
||||
p5: -99,
|
||||
p6: -7,
|
||||
p7: 9900,
|
||||
p8: -10230,
|
||||
p9: 4285,
|
||||
h2: 366, // Note they are inversed for bit packing.
|
||||
h1: 75,
|
||||
h3: 0,
|
||||
h4: 309,
|
||||
h5: 0,
|
||||
h6: 30,
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
// This data was generated with "bme280 -r"
|
||||
bus := i2ctest.Playback{
|
||||
Ops: []i2ctest.IO{
|
||||
// Chipd ID detection.
|
||||
{Addr: 0x76, Write: []byte{0xd0}, Read: []byte{0x60}},
|
||||
// Calibration data.
|
||||
{
|
||||
Addr: 0x76,
|
||||
Write: []byte{0x88},
|
||||
Read: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b},
|
||||
},
|
||||
// Calibration data.
|
||||
{Addr: 0x76, Write: []byte{0xe1}, Read: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}},
|
||||
// Configuration.
|
||||
{Addr: 0x76, Write: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xe0, 0xf4, 0x6f}, Read: nil},
|
||||
// Read.
|
||||
{Addr: 0x76, Write: []byte{0xf7}, Read: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0, 0x7a, 0x76}},
|
||||
},
|
||||
}
|
||||
dev, err := NewI2C(&bus, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env := devices.Environment{}
|
||||
if err := dev.Sense(&env); err != nil {
|
||||
t.Fatalf("Sense(): %v", err)
|
||||
}
|
||||
if env.Temperature != 23720 {
|
||||
t.Fatalf("temp %d", env.Temperature)
|
||||
}
|
||||
if env.Pressure != 100943 {
|
||||
t.Fatalf("pressure %d", env.Pressure)
|
||||
}
|
||||
if env.Humidity != 6531 {
|
||||
t.Fatalf("humidity %d", env.Humidity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalibrationFloat(t *testing.T) {
|
||||
// Real data extracted from measurements from this device.
|
||||
tRaw := int32(524112)
|
||||
pRaw := int32(309104)
|
||||
hRaw := int32(30987)
|
||||
|
||||
// Compare the values with the 3 algorithms.
|
||||
temp, tFine := calib.compensateTempFloat(tRaw)
|
||||
pres := calib.compensatePressureFloat(pRaw, tFine)
|
||||
humi := calib.compensateHumidityFloat(hRaw, tFine)
|
||||
if tFine != 117494 {
|
||||
t.Fatalf("tFine %d", tFine)
|
||||
}
|
||||
if !floatEqual(temp, 22.948120) {
|
||||
// 22.95°C
|
||||
t.Fatalf("temp %f", temp)
|
||||
}
|
||||
if !floatEqual(pres, 100.046074) {
|
||||
// 100.046kPa
|
||||
t.Fatalf("pressure %f", pres)
|
||||
}
|
||||
if !floatEqual(humi, 63.167889) {
|
||||
// 63.17%
|
||||
t.Fatalf("humidity %f", humi)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalibrationInt(t *testing.T) {
|
||||
// Real data extracted from measurements from this device.
|
||||
tRaw := int32(524112)
|
||||
pRaw := int32(309104)
|
||||
hRaw := int32(30987)
|
||||
|
||||
temp, tFine := calib.compensateTempInt(tRaw)
|
||||
pres64 := calib.compensatePressureInt64(pRaw, tFine)
|
||||
pres32 := calib.compensatePressureInt32(pRaw, tFine)
|
||||
humi := calib.compensateHumidityInt(hRaw, tFine)
|
||||
if tFine != 117407 {
|
||||
t.Fatalf("tFine %d", tFine)
|
||||
}
|
||||
if temp != 2293 {
|
||||
// 2293/100 = 22.93°C
|
||||
// Delta is <0.02°C which is pretty good.
|
||||
t.Fatalf("temp %d", temp)
|
||||
}
|
||||
if pres64 != 25611063 {
|
||||
// 25611063/256/1000 = 100.043214844
|
||||
// Delta is 3Pa which is ok.
|
||||
t.Fatalf("pressure64 %d", pres64)
|
||||
}
|
||||
if pres32 != 100045 {
|
||||
// 100045/1000 = 100.045kPa
|
||||
// Delta is 1Pa which is pretty good.
|
||||
t.Fatalf("pressure32 %d", pres32)
|
||||
}
|
||||
if humi != 64686 {
|
||||
// 64686/1024 = 63.17%
|
||||
// Delta is <0.01% which is pretty good.
|
||||
t.Fatalf("humidity %d", humi)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func Example() {
|
||||
bus, err := i2c.New(-1)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open I²C: %v", err)
|
||||
}
|
||||
defer bus.Close()
|
||||
dev, err := NewI2C(bus, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize bme280: %v", err)
|
||||
}
|
||||
env := devices.Environment{}
|
||||
if err := dev.Sense(&env); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%8s %10s %9s\n", env.Temperature, env.Pressure, env.Humidity)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
var epsilon float32 = 0.00000001
|
||||
|
||||
func floatEqual(a, b float32) bool {
|
||||
return (a-b) < epsilon && (b-a) < epsilon
|
||||
}
|
||||
|
||||
// Page 50
|
||||
|
||||
// compensatePressureInt32 returns pressure in Pa. Output value of "96386"
|
||||
// equals 96386 Pa = 963.86 hPa
|
||||
//
|
||||
// "Compensating the pressure value with 32 bit integer has an accuracy of
|
||||
// typically 1 Pa"
|
||||
//
|
||||
// raw has 20 bits of resolution.
|
||||
//
|
||||
// BUG(maruel): Output is incorrect.
|
||||
func (c *calibration) compensatePressureInt32(raw, tFine int32) uint32 {
|
||||
x := tFine>>1 - 64000
|
||||
y := (((x >> 2) * (x >> 2)) >> 11) * int32(c.p6)
|
||||
y += (x * int32(c.p5)) << 1
|
||||
y = y>>2 + int32(c.p4)<<16
|
||||
x = (((int32(c.p3) * (((x >> 2) * (x >> 2)) >> 13)) >> 3) + ((int32(c.p2) * x) >> 1)) >> 18
|
||||
x = ((32768 + x) * int32(c.p1)) >> 15
|
||||
if x == 0 {
|
||||
return 0
|
||||
}
|
||||
p := ((uint32(int32(1048576)-raw) - uint32(y>>12)) * 3125)
|
||||
if p < 0x80000000 {
|
||||
p = (p << 1) / uint32(x)
|
||||
} else {
|
||||
p = (p / uint32(x)) * 2
|
||||
}
|
||||
x = (int32(c.p9) * int32(((p>>3)*(p>>3))>>13)) >> 12
|
||||
y = (int32(p>>2) * int32(c.p8)) >> 13
|
||||
return uint32(int32(p) + ((x + y + int32(c.p7)) >> 4))
|
||||
}
|
||||
|
||||
var _ devices.Environmental = &Dev{}
|
||||
|
||||
// Page 49
|
||||
|
||||
// compensateTempFloat returns temperature in °C. Output value of "51.23"
|
||||
// equals 51.23 °C.
|
||||
//
|
||||
// raw has 20 bits of resolution.
|
||||
func (c *calibration) compensateTempFloat(raw int32) (float32, int32) {
|
||||
x := (float64(raw)/16384. - float64(c.t1)/1024.) * float64(c.t2)
|
||||
y := (float64(raw)/131072. - float64(c.t1)/8192.) * float64(c.t3)
|
||||
tFine := int32(x + y)
|
||||
return float32((x + y) / 5120.), tFine
|
||||
}
|
||||
|
||||
// compensateHumidityFloat returns pressure in Pa. Output value of "96386.2"
|
||||
// equals 96386.2 Pa = 963.862 hPa.
|
||||
//
|
||||
// raw has 20 bits of resolution.
|
||||
func (c *calibration) compensatePressureFloat(raw, tFine int32) float32 {
|
||||
x := float64(tFine)*0.5 - 64000.
|
||||
y := x * x * float64(c.p6) / 32768.
|
||||
y += x * float64(c.p5) * 2.
|
||||
y = y*0.25 + float64(c.p4)*65536.
|
||||
x = (float64(c.p3)*x*x/524288. + float64(c.p2)*x) / 524288.
|
||||
x = (1. + x/32768.) * float64(c.p1)
|
||||
if x <= 0 {
|
||||
return 0
|
||||
}
|
||||
p := float64(1048576 - raw)
|
||||
p = (p - y/4096.) * 6250. / x
|
||||
x = float64(c.p9) * p * p / 2147483648.
|
||||
y = p * float64(c.p8) / 32768.
|
||||
return float32(p+(x+y+float64(c.p7))/16.) / 1000.
|
||||
}
|
||||
|
||||
// compensateHumidityFloat returns humidity in %rH. Output value of "46.332"
|
||||
// represents 46.332 %rH.
|
||||
//
|
||||
// raw has 16 bits of resolution.
|
||||
func (c *calibration) compensateHumidityFloat(raw, tFine int32) float32 {
|
||||
h := float64(tFine - 76800)
|
||||
h = (float64(raw) - float64(c.h4)*64. + float64(c.h5)/16384.*h) * float64(c.h2) / 65536. * (1. + float64(c.h6)/67108864.*h*(1.+float64(c.h3)/67108864.*h))
|
||||
h *= 1. - float64(c.h1)*h/524288.
|
||||
if h > 100. {
|
||||
return 100.
|
||||
}
|
||||
if h < 0. {
|
||||
return 0.
|
||||
}
|
||||
return float32(h)
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
// Copyright 2016 Google Inc. 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 devices
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Display represents a pixel output device. It is a write-only interface.
|
||||
//
|
||||
// What Display represents can be as varied as a 1 bit OLED display or a strip
|
||||
// of LED lights.
|
||||
type Display interface {
|
||||
// Writer can be used when the native display pixel format is known. Each
|
||||
// write must cover exactly the whole screen as a single packed stream of
|
||||
// pixels.
|
||||
io.Writer
|
||||
// ColorModel returns the device native color model.
|
||||
//
|
||||
// It is generally color.NRGBA for a color display.
|
||||
ColorModel() color.Model
|
||||
// Bounds returns the size of the output device.
|
||||
//
|
||||
// Generally displays should have Min at {0, 0} but this is not guaranteed in
|
||||
// multiple displays setup or when an instance of this interface represents a
|
||||
// section of a larger logical display.
|
||||
Bounds() image.Rectangle
|
||||
// Draw updates the display with this image starting at 'sp' offset into the
|
||||
// display into 'r'. The code will likely be faster if the image is in the
|
||||
// display's native color format.
|
||||
//
|
||||
// To be compatible with draw.Drawer, this function doesn't return an error.
|
||||
Draw(r image.Rectangle, src image.Image, sp image.Point)
|
||||
}
|
||||
|
||||
// Milli is a fixed point value with 0.001 precision.
|
||||
type Milli int32
|
||||
|
||||
// Float64 returns the value as float64 with 0.001 precision.
|
||||
func (m Milli) Float64() float64 {
|
||||
return float64(m) * .001
|
||||
}
|
||||
|
||||
// String returns the value formatted as a string.
|
||||
func (m Milli) String() string {
|
||||
return fmt.Sprintf("%d.%03d", m/1000, m%1000)
|
||||
}
|
||||
|
||||
// Celcius is a temperature at a precision of 0.001°C.
|
||||
//
|
||||
// Expected range is [-273150, >1000000]
|
||||
//
|
||||
// BUG(maruel): Add function to convert to Fahrenheit for my American friends.
|
||||
type Celcius Milli
|
||||
|
||||
// Float64 returns the value as float64 with 0.001 precision.
|
||||
func (c Celcius) Float64() float64 {
|
||||
return Milli(c).Float64()
|
||||
}
|
||||
|
||||
// String returns the temperature formatted as a string.
|
||||
func (c Celcius) String() string {
|
||||
return Milli(c).String() + "°C"
|
||||
}
|
||||
|
||||
// ToF returns the temperature as Fahrenheit, a unit used in the United States.
|
||||
func (c Celcius) ToF() Fahrenheit {
|
||||
return Fahrenheit((c*9+2)/5 + 32000)
|
||||
}
|
||||
|
||||
// Fahrenheit is a unit used in the United States.
|
||||
type Fahrenheit Milli
|
||||
|
||||
// Float64 returns the value as float64 with 0.001 precision.
|
||||
func (f Fahrenheit) Float64() float64 {
|
||||
return Milli(f).Float64()
|
||||
}
|
||||
|
||||
// String returns the temperature formatted as a string.
|
||||
func (f Fahrenheit) String() string {
|
||||
return Milli(f).String() + "°F"
|
||||
}
|
||||
|
||||
// KPascal is pressure at precision of 1Pa.
|
||||
//
|
||||
// Expected range is [0, >1000000].
|
||||
type KPascal Milli
|
||||
|
||||
// Float64 returns the value as float64 with 0.001 precision.
|
||||
func (k KPascal) Float64() float64 {
|
||||
return Milli(k).Float64()
|
||||
}
|
||||
|
||||
// String returns the pressure formatted as a string.
|
||||
func (k KPascal) String() string {
|
||||
return Milli(k).String() + "KPa"
|
||||
}
|
||||
|
||||
// RelativeHumidity is humidity level in %rH with 0.01%rH precision.
|
||||
type RelativeHumidity int32
|
||||
|
||||
// Float64 returns the value in %.
|
||||
func (r RelativeHumidity) Float64() float64 {
|
||||
return float64(r) * .01
|
||||
}
|
||||
|
||||
// String returns the humidity formatted as a string.
|
||||
func (r RelativeHumidity) String() string {
|
||||
return fmt.Sprintf("%d.%02d%%rH", r/100, r%100)
|
||||
}
|
||||
|
||||
// Environment represents measurements from an environmental sensor.
|
||||
type Environment struct {
|
||||
Temperature Celcius
|
||||
Pressure KPascal
|
||||
Humidity RelativeHumidity
|
||||
}
|
||||
|
||||
// Environmental represents an environmental sensor.
|
||||
type Environmental interface {
|
||||
// Sense returns the value read from the sensor. Unsupported metrics are not
|
||||
// modified.
|
||||
Sense(env *Environment) error
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
// Copyright 2016 Google Inc. 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 devices
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMilli(t *testing.T) {
|
||||
o := Milli(10010)
|
||||
if s := o.String(); s != "10.010" {
|
||||
t.Fatalf("%#v", s)
|
||||
}
|
||||
if f := o.Float64(); f > 10.011 || f < 10.009 {
|
||||
t.Fatalf("%f", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCelcius(t *testing.T) {
|
||||
o := Celcius(10010)
|
||||
if s := o.String(); s != "10.010°C" {
|
||||
t.Fatalf("%#v", s)
|
||||
}
|
||||
if f := o.Float64(); f > 10.011 || f < 10.009 {
|
||||
t.Fatalf("%f", f)
|
||||
}
|
||||
if f := o.ToF(); f != 50018 {
|
||||
t.Fatalf("%d", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFahrenheit(t *testing.T) {
|
||||
o := Fahrenheit(10010)
|
||||
if s := o.String(); s != "10.010°F" {
|
||||
t.Fatalf("%#v", s)
|
||||
}
|
||||
if f := o.Float64(); f > 10.011 || f < 10.009 {
|
||||
t.Fatalf("%f", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKPascal(t *testing.T) {
|
||||
o := KPascal(10010)
|
||||
if s := o.String(); s != "10.010KPa" {
|
||||
t.Fatalf("%#v", s)
|
||||
}
|
||||
if f := o.Float64(); f > 10.011 || f < 10.009 {
|
||||
t.Fatalf("%f", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelativeHumidity(t *testing.T) {
|
||||
o := RelativeHumidity(5010)
|
||||
if s := o.String(); s != "50.10%rH" {
|
||||
t.Fatalf("%#v", s)
|
||||
}
|
||||
if f := o.Float64(); f > 50.11 || f < 50.09 {
|
||||
t.Fatalf("%f", f)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
// Copyright 2016 Google Inc. 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 devicestest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
|
||||
"github.com/google/pio/devices"
|
||||
)
|
||||
|
||||
// Display is a fake devices.Display.
|
||||
type Display struct {
|
||||
Img *image.NRGBA
|
||||
}
|
||||
|
||||
// Write implements devices.Display.
|
||||
func (d *Display) Write(pixels []byte) (int, error) {
|
||||
if len(pixels)%3 != 0 {
|
||||
return 0, errors.New("invalid RGB stream length")
|
||||
}
|
||||
copy(d.Img.Pix, pixels)
|
||||
return len(pixels), nil
|
||||
}
|
||||
|
||||
// ColorModel implements image.Image.
|
||||
func (d *Display) ColorModel() color.Model {
|
||||
return d.Img.ColorModel()
|
||||
}
|
||||
|
||||
// Bounds implements image.Image.
|
||||
func (d *Display) Bounds() image.Rectangle {
|
||||
return d.Img.Bounds()
|
||||
}
|
||||
|
||||
// Draw implements draw.Image.
|
||||
func (d *Display) Draw(r image.Rectangle, src image.Image, sp image.Point) {
|
||||
draw.Draw(d.Img, r, src, sp, draw.Src)
|
||||
}
|
||||
|
||||
var _ devices.Display = &Display{}
|
||||
@ -0,0 +1,7 @@
|
||||
// Copyright 2016 Google Inc. 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 devicestest contains non-hardware devices implementations for
|
||||
// testing or emulation purpose.
|
||||
package devicestest
|
||||
@ -0,0 +1,11 @@
|
||||
// Copyright 2016 Google Inc. 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 devices contains interfaces for classes of devices.
|
||||
//
|
||||
// Subpackages contain the concrete implementations. Devices accept bus
|
||||
// interface, constructors return concrete type.
|
||||
//
|
||||
// Subpackage devicestest contains fake implementations for testing.
|
||||
package devices
|
||||
@ -0,0 +1,48 @@
|
||||
// Copyright 2016 Google Inc. 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 lirc implements InfraRed receiver support through native linux app
|
||||
// lirc.
|
||||
//
|
||||
// Configuration
|
||||
//
|
||||
// lircd MUST be configured via TWO files: /etc/lirc/hardware.conf and
|
||||
// /etc/lirc/lircd.conf.
|
||||
//
|
||||
// See http://www.lirc.org/ for more details about daemon configuration.
|
||||
//
|
||||
// /etc/lirc/hardware.conf
|
||||
//
|
||||
// This file contains the interaction between the lircd process and the kernel
|
||||
// driver, if any. This is the link between the physical signal and decoding
|
||||
// pulses.
|
||||
//
|
||||
// /etc/lirc/lircd.conf
|
||||
//
|
||||
// This file contains all the known IR codes for the remotes you plan to use
|
||||
// and convert into key codes. This means you need to "train" lircd with the
|
||||
// remotes you plan to use.
|
||||
//
|
||||
// Keys are listed at
|
||||
// http://www.lirc.org/api-docs/html/input__map_8inc_source.html
|
||||
//
|
||||
// Debugging
|
||||
//
|
||||
// Here's a quick recipe to train a remote:
|
||||
//
|
||||
// # Detect your remote
|
||||
// irrecord -a -d /var/run/lirc/lircd ~/lircd.conf
|
||||
// # Grep for key names you found to find the remote in the remotes library
|
||||
// grep -R '<hex value>' /usr/share/lirc/remotes/
|
||||
// # Listen and send command to the server
|
||||
// nc -U /var/run/lirc/lircd
|
||||
// # List all valid key names
|
||||
// irrecord -l
|
||||
// grep -hoER '(BTN|KEY)_\w+' /usr/share/lirc/remotes | sort | uniq | less
|
||||
//
|
||||
// Raspbian
|
||||
//
|
||||
// Please see documentation of package pio/host/rpi for details on how to set
|
||||
// it up.
|
||||
package lirc
|
||||
@ -0,0 +1,275 @@
|
||||
// Copyright 2016 Google Inc. 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 lirc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/google/pio"
|
||||
"github.com/google/pio/conn/gpio"
|
||||
"github.com/google/pio/conn/ir"
|
||||
)
|
||||
|
||||
// Conn is an open port to lirc.
|
||||
type Conn struct {
|
||||
w net.Conn
|
||||
c chan ir.Message
|
||||
lock sync.Mutex
|
||||
list map[string][]string // list of remotes and associated keys
|
||||
pendingList map[string][]string // list of remotes and associated keys being created.
|
||||
}
|
||||
|
||||
// New returns a IR receiver / emitter handle.
|
||||
func New() (*Conn, error) {
|
||||
w, err := net.Dial("unix", "/var/run/lirc/lircd")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Conn{w: w, c: make(chan ir.Message), list: map[string][]string{}}
|
||||
// Inconditionally retrieve the list of all known keys at start.
|
||||
if _, err := w.Write([]byte("LIST\n")); err != nil {
|
||||
w.Close()
|
||||
return nil, err
|
||||
}
|
||||
go c.loop(bufio.NewReader(w))
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Close closes the socket to lirc. It is not a requirement to close before
|
||||
// process termination.
|
||||
func (c *Conn) Close() error {
|
||||
return c.w.Close()
|
||||
}
|
||||
|
||||
// Emit implements ir.IR.
|
||||
func (c *Conn) Emit(remote string, key ir.Key) error {
|
||||
// http://www.lirc.org/html/lircd.html#lbAH
|
||||
_, err := fmt.Fprintf(c.w, "SEND_ONCE %s %s", remote, key)
|
||||
return err
|
||||
}
|
||||
|
||||
// Channel implements ir.IR.
|
||||
func (c *Conn) Channel() <-chan ir.Message {
|
||||
return c.c
|
||||
}
|
||||
|
||||
// Codes returns all the known codes.
|
||||
//
|
||||
// Empty if the list was not retrieved yet.
|
||||
func (c *Conn) Codes() map[string][]string {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
return c.list
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Conn) loop(r *bufio.Reader) {
|
||||
defer func() {
|
||||
close(c.c)
|
||||
c.c = nil
|
||||
}()
|
||||
for {
|
||||
line, err := read(r)
|
||||
if line == "BEGIN" {
|
||||
err = c.readData(r)
|
||||
} else if len(line) != 0 {
|
||||
// Format is: <code> <repeat count> <button name> <remote control name>
|
||||
// http://www.lirc.org/html/lircd.html#lbAG
|
||||
if parts := strings.SplitN(line, " ", 5); len(parts) != 4 {
|
||||
log.Printf("ir: corrupted line: %v", line)
|
||||
} else {
|
||||
if i, err2 := strconv.Atoi(parts[1]); err2 != nil {
|
||||
log.Printf("ir: corrupted line: %v", line)
|
||||
} else if len(parts[2]) != 0 && len(parts[3]) != 0 {
|
||||
c.c <- ir.Message{Key: ir.Key(parts[2]), RemoteType: parts[3], Repeat: i != 0}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) readData(r *bufio.Reader) error {
|
||||
// Format is:
|
||||
// BEGIN
|
||||
// <original command>
|
||||
// SUCCESS
|
||||
// DATA
|
||||
// <number of entries 1 based>
|
||||
// <entries>
|
||||
// ...
|
||||
// END
|
||||
cmd, err := read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch cmd {
|
||||
case "SIGHUP":
|
||||
_, err = c.w.Write([]byte("LIST\n"))
|
||||
default:
|
||||
// In case of any error, ignore the rest.
|
||||
line, err := read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if line != "SUCCESS" {
|
||||
log.Printf("ir: unexpected line: %v, expected SUCCESS", line)
|
||||
return nil
|
||||
}
|
||||
if line, err = read(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if line != "DATA" {
|
||||
log.Printf("ir: unexpected line: %v, expected DATA", line)
|
||||
return nil
|
||||
}
|
||||
if line, err = read(r); err != nil {
|
||||
return err
|
||||
}
|
||||
nbLines, err := strconv.Atoi(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list := make([]string, nbLines)
|
||||
for i := 0; i < nbLines; i++ {
|
||||
if list[i], err = read(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case cmd == "LIST":
|
||||
// Request the codes for each remote.
|
||||
c.pendingList = map[string][]string{}
|
||||
for _, l := range list {
|
||||
if _, ok := c.pendingList[l]; ok {
|
||||
log.Printf("ir: unexpected %s", cmd)
|
||||
} else {
|
||||
c.pendingList[l] = []string{}
|
||||
if _, err = fmt.Fprintf(c.w, "LIST %s\n", l); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case strings.HasPrefix(line, "LIST "):
|
||||
if c.pendingList == nil {
|
||||
log.Printf("ir: unexpected %s", cmd)
|
||||
} else {
|
||||
remote := cmd[5:]
|
||||
c.pendingList[remote] = list
|
||||
all := true
|
||||
for _, v := range c.pendingList {
|
||||
if len(v) == 0 {
|
||||
all = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if all {
|
||||
c.lock.Lock()
|
||||
c.list = c.pendingList
|
||||
c.pendingList = nil
|
||||
c.lock.Unlock()
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
line, err := read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if line != "END" {
|
||||
log.Printf("ir: unexpected line: %v, expected END", line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func read(r *bufio.Reader) (string, error) {
|
||||
raw, err := r.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(raw) != 0 {
|
||||
raw = raw[:len(raw)-1]
|
||||
}
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
// driver implements pio.Driver.
|
||||
type driver struct {
|
||||
}
|
||||
|
||||
func (d *driver) String() string {
|
||||
return "lirc"
|
||||
}
|
||||
|
||||
func (d *driver) Type() pio.Type {
|
||||
// Return the lowest priority, which is Functional.
|
||||
return pio.Functional
|
||||
}
|
||||
|
||||
func (d *driver) Init() (bool, error) {
|
||||
in, out := getPins()
|
||||
if in == -1 && out == -1 {
|
||||
return false, nil
|
||||
}
|
||||
if in != -1 {
|
||||
if pin := gpio.ByNumber(in); pin != nil {
|
||||
gpio.MapFunction("IR_IN", pin)
|
||||
} else {
|
||||
gpio.MapFunction("IR_IN", gpio.INVALID)
|
||||
}
|
||||
} else {
|
||||
gpio.MapFunction("IR_IN", gpio.INVALID)
|
||||
}
|
||||
if out != -1 {
|
||||
if pin := gpio.ByNumber(out); pin != nil {
|
||||
gpio.MapFunction("IR_OUT", pin)
|
||||
} else {
|
||||
gpio.MapFunction("IR_OUT", gpio.INVALID)
|
||||
}
|
||||
} else {
|
||||
gpio.MapFunction("IR_OUT", gpio.INVALID)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getPins queries the kernel module to determine which GPIO pins are taken by
|
||||
// the driver.
|
||||
//
|
||||
// The return values can be converted to bcm238x.Pin. Return (-1, -1) on
|
||||
// failure.
|
||||
func getPins() (int, int) {
|
||||
// This is configured in /boot/config.txt as:
|
||||
// dtoverlay=gpio_in_pin=23,gpio_out_pin=22
|
||||
bytes, err := ioutil.ReadFile("/sys/module/lirc_rpi/parameters/gpio_in_pin")
|
||||
if err != nil || len(bytes) == 0 {
|
||||
return -1, -1
|
||||
}
|
||||
in, err := strconv.Atoi(strings.TrimRight(string(bytes), "\n"))
|
||||
if err != nil {
|
||||
return -1, -1
|
||||
}
|
||||
bytes, err = ioutil.ReadFile("/sys/module/lirc_rpi/parameters/gpio_out_pin")
|
||||
if err != nil || len(bytes) == 0 {
|
||||
return -1, -1
|
||||
}
|
||||
out, err := strconv.Atoi(strings.TrimRight(string(bytes), "\n"))
|
||||
if err != nil {
|
||||
return -1, -1
|
||||
}
|
||||
return in, out
|
||||
}
|
||||
|
||||
var _ ir.Conn = &Conn{}
|
||||
@ -0,0 +1,145 @@
|
||||
// Copyright 2016 Google Inc. 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 image1bit implements black and white (1 bit per pixel) 2D graphics
|
||||
// in the memory format of the ssd1306 controller.
|
||||
//
|
||||
// It is compatible with package image/draw.
|
||||
package image1bit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
)
|
||||
|
||||
// Bit implements a 1 bit color.
|
||||
type Bit bool
|
||||
|
||||
// RGBA returns either all white or all black and transparent.
|
||||
func (b Bit) RGBA() (uint32, uint32, uint32, uint32) {
|
||||
if b {
|
||||
return 65535, 65535, 65535, 65535
|
||||
}
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
|
||||
func (b Bit) String() string {
|
||||
if b {
|
||||
return "On"
|
||||
}
|
||||
return "Off"
|
||||
}
|
||||
|
||||
// Possible bitness.
|
||||
const (
|
||||
On = Bit(true)
|
||||
Off = Bit(false)
|
||||
)
|
||||
|
||||
// Image is a 1 bit (black and white) image.
|
||||
//
|
||||
// The packing used is unusual, each byte is 8 vertical pixels, with each byte
|
||||
// stride being an horizontal band of 8 pixels high.
|
||||
//
|
||||
// It is designed specifically to work with SSD1306 OLED display controler.
|
||||
type Image struct {
|
||||
W int
|
||||
H int
|
||||
Buf []byte // Can be passed directly to ssd1306.(*Dev).Write()
|
||||
}
|
||||
|
||||
// New returns an initialized Image instance.
|
||||
func New(r image.Rectangle) (*Image, error) {
|
||||
h := r.Dy()
|
||||
w := r.Dx()
|
||||
if h&7 != 0 {
|
||||
return nil, errors.New("height must be multiple of 8")
|
||||
}
|
||||
return &Image{w, h, make([]byte, w*h/8)}, nil
|
||||
}
|
||||
|
||||
// SetAll sets all pixels to On.
|
||||
func (i *Image) SetAll() {
|
||||
for j := range i.Buf {
|
||||
i.Buf[j] = 0xFF
|
||||
}
|
||||
}
|
||||
|
||||
// Clear sets all pixels to Off.
|
||||
func (i *Image) Clear() {
|
||||
for j := range i.Buf {
|
||||
i.Buf[j] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Inverse changes all On pixels to Off and Off pixels to On.
|
||||
func (i *Image) Inverse() {
|
||||
for j := range i.Buf {
|
||||
i.Buf[j] ^= 0xFF
|
||||
}
|
||||
}
|
||||
|
||||
// ColorModel implements image.Image.
|
||||
func (i *Image) ColorModel() color.Model {
|
||||
return color.ModelFunc(convert)
|
||||
}
|
||||
|
||||
// Bounds implements image.Image.
|
||||
func (i *Image) Bounds() image.Rectangle {
|
||||
return image.Rectangle{Max: image.Point{X: i.W, Y: i.H}}
|
||||
}
|
||||
|
||||
// At implements image.Image.
|
||||
func (i *Image) At(x, y int) color.Color {
|
||||
return i.AtBit(x, y)
|
||||
}
|
||||
|
||||
// AtBit is the optimized version of At().
|
||||
func (i *Image) AtBit(x, y int) Bit {
|
||||
offset := x + y/8*i.W
|
||||
mask := byte(1 << byte(y&7))
|
||||
return Bit(i.Buf[offset]&mask != 0)
|
||||
}
|
||||
|
||||
// Set implements draw.Image
|
||||
func (i *Image) Set(x, y int, c color.Color) {
|
||||
i.SetBit(x, y, convertBit(c))
|
||||
}
|
||||
|
||||
// SetBit is the optimized version of Set().
|
||||
func (i *Image) SetBit(x, y int, b Bit) {
|
||||
if x >= 0 && x < i.W {
|
||||
if y >= 0 && y < i.H {
|
||||
offset := x + y/8*i.W
|
||||
mask := byte(1 << byte(y&7))
|
||||
if b {
|
||||
i.Buf[offset] |= mask
|
||||
} else {
|
||||
i.Buf[offset] &^= mask
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
var _ draw.Image = &Image{}
|
||||
|
||||
// Anything not transparent and not pure black is white.
|
||||
func convert(c color.Color) color.Color {
|
||||
return convertBit(c)
|
||||
}
|
||||
|
||||
// Anything not transparent and not pure black is white.
|
||||
func convertBit(c color.Color) Bit {
|
||||
switch t := c.(type) {
|
||||
case Bit:
|
||||
return t
|
||||
default:
|
||||
r, g, b, _ := c.RGBA()
|
||||
return Bit((r | g | b) >= 0x8000)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
// Copyright 2016 Google Inc. 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 image1bit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBit(t *testing.T) {
|
||||
if r, g, b, a := On.RGBA(); r != 65535 || g != r || b != r || a != r {
|
||||
t.Fail()
|
||||
}
|
||||
if r, g, b, a := Off.RGBA(); r != 0 || g != r || b != r || a != r {
|
||||
t.Fail()
|
||||
}
|
||||
if On.String() != "On" || Off.String() != "Off" {
|
||||
t.Fail()
|
||||
}
|
||||
if On != convertBit(On) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
func TestImageNew(t *testing.T) {
|
||||
if img, err := New(image.Rect(0, 0, 8, 7)); img != nil || err == nil {
|
||||
t.Fail()
|
||||
}
|
||||
if img, err := New(image.Rect(0, 0, 1, 8)); img == nil || err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestImagePixels(t *testing.T) {
|
||||
img, _ := New(image.Rect(0, 0, 1, 8))
|
||||
if !bytes.Equal(img.Buf, []byte{0x00}) {
|
||||
t.Fatal("starts black")
|
||||
}
|
||||
img.SetAll()
|
||||
if !bytes.Equal(img.Buf, []byte{0xFF}) {
|
||||
t.Fatal("SetAll sets white")
|
||||
}
|
||||
img.Clear()
|
||||
if !bytes.Equal(img.Buf, []byte{0x00}) {
|
||||
t.Fatal("Clear sets black")
|
||||
}
|
||||
img.Set(0, 2, color.NRGBA{0x80, 0x80, 0x80, 0xFF})
|
||||
img.Set(1, 2, color.NRGBA{0x80, 0x80, 0x80, 0xFF})
|
||||
img.Inverse()
|
||||
if !bytes.Equal(img.Buf, []byte{0xFB}) {
|
||||
t.Fatalf("inverse %# v", img.Buf)
|
||||
}
|
||||
if img.At(0, 2).(Bit) != Off {
|
||||
t.Fail()
|
||||
}
|
||||
if r := img.Bounds(); r.Min.X != 0 || r.Min.Y != 0 || r.Max.X != 1 || r.Max.Y != 8 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorModel(t *testing.T) {
|
||||
img, _ := New(image.Rect(0, 0, 1, 8))
|
||||
if v := img.ColorModel().Convert(color.NRGBA{0x80, 0x80, 0x80, 0xFF}).(Bit); v != On {
|
||||
t.Fatalf("%s", v)
|
||||
}
|
||||
if v := img.ColorModel().Convert(color.NRGBA{0x7F, 0x7F, 0x7F, 0xFF}).(Bit); v != Off {
|
||||
t.Fatalf("%s", v)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,294 @@
|
||||
// Copyright 2016 Google Inc. 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 ssd1306 controls a 128x64 monochrome OLED display via a ssd1306
|
||||
// controler.
|
||||
//
|
||||
// The SSD1306 is a write-only device. It can be driven on either I²C or SPI.
|
||||
// Changing between protocol is likely done through resistor soldering, for
|
||||
// boards that support both.
|
||||
//
|
||||
// Datasheet
|
||||
//
|
||||
// https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
|
||||
package ssd1306
|
||||
|
||||
// Some have SPI enabled;
|
||||
// https://hallard.me/adafruit-oled-display-driver-for-pi/
|
||||
// https://learn.adafruit.com/ssd1306-oled-displays-with-raspberry-pi-and-beaglebone-black?view=all
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"github.com/google/pio/conn/i2c"
|
||||
"github.com/google/pio/conn/spi"
|
||||
"github.com/google/pio/devices"
|
||||
"github.com/google/pio/devices/ssd1306/image1bit"
|
||||
)
|
||||
|
||||
// FrameRate determines scrolling speed.
|
||||
type FrameRate byte
|
||||
|
||||
// Possible frame rates.
|
||||
const (
|
||||
FrameRate2 FrameRate = 7
|
||||
FrameRate3 FrameRate = 4
|
||||
FrameRate4 FrameRate = 5
|
||||
FrameRate5 FrameRate = 0
|
||||
FrameRate25 FrameRate = 6
|
||||
FrameRate64 FrameRate = 1
|
||||
FrameRate128 FrameRate = 2
|
||||
FrameRate256 FrameRate = 3
|
||||
)
|
||||
|
||||
// Orientation is used for scrolling.
|
||||
type Orientation byte
|
||||
|
||||
// Possible orientations for scrolling.
|
||||
const (
|
||||
Left Orientation = 0x27
|
||||
Right Orientation = 0x26
|
||||
UpRight Orientation = 0x29
|
||||
UpLeft Orientation = 0x2A
|
||||
)
|
||||
|
||||
// Dev is an open handle to the display controler.
|
||||
type Dev struct {
|
||||
w io.Writer
|
||||
W int
|
||||
H int
|
||||
}
|
||||
|
||||
// NewSPI returns a Dev object that communicates over SPI to SSD1306 display
|
||||
// controler.
|
||||
//
|
||||
// If rotated, turns the display by 180°
|
||||
//
|
||||
// It's up to the caller to use the RES (reset) pin if desired. Simpler
|
||||
// connection is to connect RES and DC to ground, CS to 3.3v, SDA to MOSI, SCK
|
||||
// to SCLK.
|
||||
//
|
||||
// As per datasheet, maximum clock speed is 1/100ns = 10MHz.
|
||||
func NewSPI(s spi.Conn, w, h int, rotated bool) (*Dev, error) {
|
||||
if err := s.Configure(spi.Mode3, 8); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newDev(s, w, h, rotated)
|
||||
}
|
||||
|
||||
// NewI2C returns a Dev object that communicates over I²C to SSD1306 display
|
||||
// controler.
|
||||
//
|
||||
// If rotated, turns the display by 180°
|
||||
//
|
||||
// As per datasheet, maximum clock speed is 1/2.5µs = 400KHz. It's worth
|
||||
// bumping up from default bus speed of 100KHz if possible.
|
||||
func NewI2C(i i2c.Conn, w, h int, rotated bool) (*Dev, error) {
|
||||
return newDev(&i2c.Dev{Conn: i, Addr: 0x3C}, w, h, rotated)
|
||||
}
|
||||
|
||||
// newDev is the common initialization code that is independent of the bus
|
||||
// being used.
|
||||
func newDev(dev io.Writer, w, h int, rotated bool) (*Dev, error) {
|
||||
if w&7 != 0 || h&7 != 0 {
|
||||
return nil, errors.New("height and width must be multiple of 8")
|
||||
}
|
||||
if w < 8 || w > 128 {
|
||||
return nil, errors.New("invalid height")
|
||||
}
|
||||
if h < 8 || h > 64 {
|
||||
return nil, errors.New("invalid width")
|
||||
}
|
||||
|
||||
d := &Dev{w: dev, W: w, H: h}
|
||||
|
||||
contrast := byte(0x7F) // (default value)
|
||||
|
||||
// Set COM output scan direction; C0 means normal; C8 means reversed
|
||||
comScan := byte(0xC8)
|
||||
// See page 40.
|
||||
columnAddr := byte(0xA1)
|
||||
if rotated {
|
||||
// Change order both horizontally and vertically.
|
||||
comScan = 0xC0
|
||||
columnAddr = byte(0xA0)
|
||||
}
|
||||
// Initialize the device by fully reseting all values.
|
||||
// https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
|
||||
// Page 64 has the full recommended flow.
|
||||
// Page 28 lists all the commands.
|
||||
// BUG(maruel): This flow may not recover a controller in a complete
|
||||
// corrupted state. Figure out a more resilient startup init code.
|
||||
init := []byte{
|
||||
0xAE, // Display off
|
||||
0xD3, 0x00, // Set display offset; 0
|
||||
0x40, // Start display start line; 0
|
||||
columnAddr, // Set segment remap; RESET is column 127.
|
||||
comScan,
|
||||
0xDA, 0x12, // Set COM pins hardware configuration; see page 40
|
||||
0x81, contrast, // Set contrast control
|
||||
0xA4, // Set display to use GDDRAM content
|
||||
0xA6, // Set normal display (0xA7 for reversed bitness i.e. bit set is black) (?)
|
||||
0xD5, 0x40, // Set osc frequency and divide ratio; power on reset value is 0x3F.
|
||||
0x8D, 0x14, // Enable charge pump regulator; page 62
|
||||
|
||||
// Not sure
|
||||
0xD9, 0xF1, // Set pre-charge period.
|
||||
//0xDB, 0x40, // Set Vcomh deselect level; page 32
|
||||
0x20, 0x00, // Set memory addressing mode to horizontal (can be page, horizontal or vertical)
|
||||
0x2E, // Deactivate scroll
|
||||
0x00 | 0x00, // Set column offset (lower nibble)
|
||||
0x10 | 0x00, // Set column offset (higher nibble)
|
||||
0xA8, byte(d.H - 1), // Set multiplex ratio (number of lines to display)
|
||||
0xAF, // Display on
|
||||
}
|
||||
if _, err := d.w.Write(init); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ColorModel implements devices.Display. It is a one bit color model.
|
||||
func (d *Dev) ColorModel() color.Model {
|
||||
return color.NRGBAModel
|
||||
}
|
||||
|
||||
// Bounds implements devices.Display. Min is guaranteed to be {0, 0}.
|
||||
func (d *Dev) Bounds() image.Rectangle {
|
||||
return image.Rectangle{Max: image.Point{X: d.W, Y: d.H}}
|
||||
}
|
||||
|
||||
func colorToBit(c color.Color) byte {
|
||||
r, g, b, a := c.RGBA()
|
||||
if (r|g|b) >= 0x8000 && a >= 0x4000 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Draw implements devices.Display.
|
||||
//
|
||||
// BUG(maruel): It discards any failure. Change devices.Display interface?
|
||||
// BUG(maruel): Support r.Min.Y and r.Max.Y not divisible by 8.
|
||||
// BUG(maruel): Support sp.Y not divisible by 8.
|
||||
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
|
||||
r = r.Intersect(d.Bounds())
|
||||
srcR := src.Bounds()
|
||||
srcR.Min = srcR.Min.Add(sp)
|
||||
if dX := r.Dx(); dX < srcR.Dx() {
|
||||
srcR.Max.X = srcR.Min.X + dX
|
||||
}
|
||||
if dY := r.Dy(); dY < srcR.Dy() {
|
||||
srcR.Max.Y = srcR.Min.Y + dY
|
||||
}
|
||||
// Take 8 lines at a time.
|
||||
deltaX := r.Min.X - srcR.Min.X
|
||||
deltaY := r.Min.Y - srcR.Min.Y
|
||||
|
||||
var pixels []byte
|
||||
if img, ok := src.(*image1bit.Image); ok {
|
||||
if srcR.Min.X == 0 && srcR.Dx() == d.W && srcR.Min.Y == 0 && srcR.Dy() == d.H {
|
||||
// Fast path.
|
||||
pixels = img.Buf
|
||||
}
|
||||
}
|
||||
if pixels == nil {
|
||||
pixels = make([]byte, d.W*d.H/8)
|
||||
for sY := srcR.Min.Y; sY < srcR.Max.Y; sY += 8 {
|
||||
rY := ((sY + deltaY) / 8) * d.W
|
||||
for sX := srcR.Min.X; sX < srcR.Max.X; sX++ {
|
||||
rX := sX + deltaX
|
||||
c0 := colorToBit(src.At(sX, sY))
|
||||
c1 := colorToBit(src.At(sX, sY+1)) << 1
|
||||
c2 := colorToBit(src.At(sX, sY+2)) << 2
|
||||
c3 := colorToBit(src.At(sX, sY+3)) << 3
|
||||
c4 := colorToBit(src.At(sX, sY+4)) << 4
|
||||
c5 := colorToBit(src.At(sX, sY+5)) << 5
|
||||
c6 := colorToBit(src.At(sX, sY+6)) << 6
|
||||
c7 := colorToBit(src.At(sX, sY+7)) << 7
|
||||
pixels[rX+rY] = c0 | c1 | c2 | c3 | c4 | c5 | c6 | c7
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := d.Write(pixels); err != nil {
|
||||
log.Printf("ssd1306: Draw failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes a buffer of pixels to the display.
|
||||
//
|
||||
// The format is unsual as each byte represent 8 vertical pixels at a time. So
|
||||
// the memory is effectively horizontal bands of 8 pixels high.
|
||||
func (d *Dev) Write(pixels []byte) (int, error) {
|
||||
if len(pixels) != d.H*d.W/8 {
|
||||
return 0, errors.New("invalid pixel stream")
|
||||
}
|
||||
|
||||
// Run as 2 big transactions to reduce downtime on the bus. Doing with one
|
||||
// transaction doesn't work (?)
|
||||
hdr := []byte{
|
||||
0x21, 0x00, byte(d.W - 1), // Set column address (Width)
|
||||
0x22, 0x00, byte(d.H/8 - 1), // Set page address (Pages)
|
||||
}
|
||||
if _, err := d.w.Write(hdr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err := d.w.Write(append([]byte{0x40}, pixels...)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(pixels), nil
|
||||
}
|
||||
|
||||
// Scroll scrolls the entire.
|
||||
func (d *Dev) Scroll(o Orientation, rate FrameRate) error {
|
||||
// TODO(maruel): Allow to specify page.
|
||||
// TODO(maruel): Allow to specify offset.
|
||||
if o == Left || o == Right {
|
||||
// page 28
|
||||
// STOP, <op>, dummy, <start page>, <rate>, <end page>, <dummy>, <dummy>, <ENABLE>
|
||||
_, err := d.w.Write([]byte{0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x00, 0xFF, 0x2F})
|
||||
return err
|
||||
}
|
||||
// page 29
|
||||
// STOP, <op>, dummy, <start page>, <rate>, <end page>, <offset>, <ENABLE>
|
||||
// page 30: 0xA3 permits to set rows for scroll area.
|
||||
_, err := d.w.Write([]byte{0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x01, 0x2F})
|
||||
return err
|
||||
}
|
||||
|
||||
// StopScroll stops any scrolling previously set.
|
||||
//
|
||||
// It will only take effect after redrawing the ram.
|
||||
//
|
||||
// BUG(maruel): Doesn't work.
|
||||
func (d *Dev) StopScroll() error {
|
||||
_, err := d.w.Write([]byte{0x2E})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetContrast changes the screen contrast.
|
||||
//
|
||||
// BUG(maruel): Doesn't work.
|
||||
func (d *Dev) SetContrast(level byte) error {
|
||||
_, err := d.w.Write([]byte{0x81, level})
|
||||
return err
|
||||
}
|
||||
|
||||
// Enable or disable the display.
|
||||
//
|
||||
// BUG(maruel): Doesn't work.
|
||||
func (d *Dev) Enable(on bool) error {
|
||||
b := byte(0xAE)
|
||||
if on {
|
||||
b = 0xAF
|
||||
}
|
||||
_, err := d.w.Write([]byte{b})
|
||||
return err
|
||||
}
|
||||
|
||||
var _ devices.Display = &Dev{}
|
||||
@ -0,0 +1,192 @@
|
||||
// Copyright 2016 Google Inc. 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 ssd1306
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/google/pio/conn/i2c"
|
||||
"github.com/google/pio/conn/i2c/i2ctest"
|
||||
"github.com/google/pio/devices/ssd1306/image1bit"
|
||||
)
|
||||
|
||||
func TestDrawGray(t *testing.T) {
|
||||
// ssd1306 is a write-only device. This is simply a playback test to exercise
|
||||
// the code a little.
|
||||
bus := i2ctest.Playback{
|
||||
Ops: []i2ctest.IO{
|
||||
// Startup initialization.
|
||||
{Addr: 0x3c, Write: initline},
|
||||
// Preparation to draw.
|
||||
{Addr: 0x3c, Write: []byte{0x21, 0x0, 0x7f, 0x22, 0x0, 0x7}},
|
||||
// Actual draw buffer.
|
||||
{Addr: 0x3c, Write: grayCheckboardWrite},
|
||||
},
|
||||
}
|
||||
// To record, use the following instead:
|
||||
//bus := i2ctest.Record{}
|
||||
dev, err := NewI2C(&bus, 128, 64, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{0, 0})
|
||||
if err := bus.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
//pretty.Printf("%# v\n", bus)
|
||||
}
|
||||
|
||||
func TestDraw1D(t *testing.T) {
|
||||
bus := i2ctest.Playback{
|
||||
Ops: []i2ctest.IO{
|
||||
{Addr: 0x3c, Write: initline},
|
||||
{Addr: 0x3c, Write: []byte{0x21, 0x0, 0x7f, 0x22, 0x0, 0x7}},
|
||||
{Addr: 0x3c, Write: grayCheckboardWrite},
|
||||
},
|
||||
}
|
||||
dev, err := NewI2C(&bus, 128, 64, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bounds := dev.Bounds()
|
||||
gray := makeGrayCheckboard(bounds)
|
||||
img, err := image1bit.New(bounds)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
img.Set(x, y, gray.At(x, y))
|
||||
}
|
||||
}
|
||||
dev.Draw(dev.Bounds(), img, image.Point{0, 0})
|
||||
if err := bus.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func Example() {
|
||||
bus, err := i2c.New(-1)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open I²C: %v", err)
|
||||
}
|
||||
defer bus.Close()
|
||||
dev, err := NewI2C(bus, 128, 64, false)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize ssd1306: %v", err)
|
||||
}
|
||||
|
||||
// Draw on it.
|
||||
f := basicfont.Face7x13
|
||||
img, err := image1bit.New(dev.Bounds())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
drawer := font.Drawer{
|
||||
Dst: img,
|
||||
Src: &image.Uniform{image1bit.On},
|
||||
Face: f,
|
||||
Dot: fixed.P(0, img.Bounds().Dy()-1-f.Descent),
|
||||
}
|
||||
drawer.DrawString("Hello from pio!")
|
||||
dev.Draw(dev.Bounds(), img, image.Point{})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func makeGrayCheckboard(r image.Rectangle) image.Image {
|
||||
img := image.NewGray(r)
|
||||
c := color.Gray{255}
|
||||
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||
for x := r.Min.X; x < r.Max.X; x++ {
|
||||
if (y+x)&1 != 0 {
|
||||
img.SetGray(x, y, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
var initline = []byte{
|
||||
0xae, 0xd3, 0x00, 0x40, 0xa1, 0xc8, 0xda, 0x12, 0x81, 0x7f, 0xa4, 0xa6, 0xd5,
|
||||
0x40, 0x8d, 0x14, 0xd9, 0xf1, 0x20, 0x00, 0x2e, 0x00, 0x10, 0xa8, 0x3f, 0xaf,
|
||||
}
|
||||
|
||||
var grayCheckboardWrite = []byte{
|
||||
0x40,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
// Copyright 2016 Google Inc. 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 tm1637 controls a TM1637 device over GPIO pins.
|
||||
//
|
||||
// Datasheet
|
||||
//
|
||||
// http://olimex.cl/website_MCI/static/documents/Datasheet_TM1637.pdf
|
||||
package tm1637
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/google/pio/conn/gpio"
|
||||
"github.com/google/pio/host"
|
||||
)
|
||||
|
||||
// Clock converts time to a slice of bytes as segments.
|
||||
func Clock(hour, minute int, showDots bool) []byte {
|
||||
seg := make([]byte, 4)
|
||||
seg[0] = byte(digitToSegment[hour/10])
|
||||
seg[1] = byte(digitToSegment[hour%10])
|
||||
seg[2] = byte(digitToSegment[minute/10])
|
||||
seg[3] = byte(digitToSegment[minute%10])
|
||||
if showDots {
|
||||
seg[1] |= 0x80
|
||||
}
|
||||
return seg[:]
|
||||
}
|
||||
|
||||
// Digits converts hex numbers to a slice of bytes as segments.
|
||||
//
|
||||
// Numbers outside the range [0, 15] are displayed as blank. Use -1 to mark it
|
||||
// as blank.
|
||||
func Digits(n ...int) []byte {
|
||||
seg := make([]byte, len(n))
|
||||
for i := range n {
|
||||
if n[i] >= 0 && n[i] < 16 {
|
||||
seg[i] = byte(digitToSegment[n[i]])
|
||||
}
|
||||
}
|
||||
return seg
|
||||
}
|
||||
|
||||
// Brightness defines the screen brightness as controlled by the internal PWM.
|
||||
type Brightness uint8
|
||||
|
||||
// Valid brightness values.
|
||||
const (
|
||||
Off Brightness = 0x80 // Completely off.
|
||||
Brightness1 Brightness = 0x88 // 1/16 PWM
|
||||
Brightness2 Brightness = 0x89 // 2/16 PWM
|
||||
Brightness4 Brightness = 0x8A // 4/16 PWM
|
||||
Brightness10 Brightness = 0x8B // 10/16 PWM
|
||||
Brightness11 Brightness = 0x8C // 11/16 PWM
|
||||
Brightness12 Brightness = 0x8D // 12/16 PWM
|
||||
Brightness13 Brightness = 0x8E // 13/16 PWM
|
||||
Brightness14 Brightness = 0x8F // 14/16 PWM
|
||||
)
|
||||
|
||||
// Dev represents an handle to a tm1637.
|
||||
type Dev struct {
|
||||
clk gpio.PinOut
|
||||
data gpio.PinIO
|
||||
}
|
||||
|
||||
// SetBrightness changes the brightness and/or turns the display on and off.
|
||||
func (d *Dev) SetBrightness(b Brightness) error {
|
||||
// This helps reduce jitter a little.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
d.start()
|
||||
d.writeByte(byte(b))
|
||||
d.stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes raw segments, while implementing io.Writer.
|
||||
//
|
||||
// P can be a dot or ':' following a digit. Otherwise it is likely
|
||||
// disconnected. Each byte is encoded as PGFEDCBA.
|
||||
//
|
||||
// -A-
|
||||
// F B
|
||||
// -G-
|
||||
// E C
|
||||
// -D- P
|
||||
func (d *Dev) Write(seg []byte) (int, error) {
|
||||
if len(seg) > 6 {
|
||||
return 0, errors.New("tm1637: up to 6 segment groups are supported")
|
||||
}
|
||||
// This helps reduce jitter a little.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
// Use auto-incrementing address. It is possible to write to a single
|
||||
// segment but there isn't much point.
|
||||
d.start()
|
||||
d.writeByte(0x40)
|
||||
d.stop()
|
||||
d.start()
|
||||
d.writeByte(0xC0)
|
||||
for i := 0; i < 6; i++ {
|
||||
if len(seg) <= i {
|
||||
d.writeByte(0)
|
||||
} else {
|
||||
d.writeByte(seg[i])
|
||||
}
|
||||
}
|
||||
d.stop()
|
||||
return len(seg), nil
|
||||
}
|
||||
|
||||
// New returns an object that communicates over two pins to a TM1637.
|
||||
func New(clk gpio.PinOut, data gpio.PinIO) (*Dev, error) {
|
||||
// Spec calls to idle at high.
|
||||
if err := clk.Out(gpio.High); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := data.Out(gpio.High); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &Dev{clk: clk, data: data}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// Page 10 states the max clock frequency is 500KHz but page 3 states 250KHz.
|
||||
//
|
||||
// Writing the complete display is 8 bytes, totalizing 9*8+2 = 74 cycles.
|
||||
// At 250KHz, this is 296µs.
|
||||
const clockHalfCycle = time.Second / 250000 / 2
|
||||
|
||||
// Hex digits from 0 to F.
|
||||
var digitToSegment = []byte{
|
||||
0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71,
|
||||
}
|
||||
|
||||
func (d *Dev) start() {
|
||||
d.data.Out(gpio.Low)
|
||||
d.sleepHalfCycle()
|
||||
d.clk.Out(gpio.Low)
|
||||
}
|
||||
|
||||
func (d *Dev) stop() {
|
||||
d.sleepHalfCycle()
|
||||
d.clk.Out(gpio.High)
|
||||
d.sleepHalfCycle()
|
||||
d.data.Out(gpio.High)
|
||||
d.sleepHalfCycle()
|
||||
}
|
||||
|
||||
// writeByte starts with d.data low and d.clk high and ends with d.data low and
|
||||
// d.clk high.
|
||||
func (d *Dev) writeByte(b byte) (bool, error) {
|
||||
for i := 0; i < 8; i++ {
|
||||
// LSB (!)
|
||||
d.data.Out(b&(1<<byte(i)) != 0)
|
||||
d.sleepHalfCycle()
|
||||
d.clk.Out(gpio.High)
|
||||
d.sleepHalfCycle()
|
||||
d.clk.Out(gpio.Low)
|
||||
}
|
||||
// 9th clock is ACK.
|
||||
d.data.Out(gpio.Low)
|
||||
time.Sleep(clockHalfCycle)
|
||||
// TODO(maruel): Add.
|
||||
//if err := d.data.In(gpio.Up, gpio.None); err != nil {
|
||||
// return false, err
|
||||
//}
|
||||
d.clk.Out(gpio.High)
|
||||
d.sleepHalfCycle()
|
||||
//ack := d.data.Read() == gpio.Low
|
||||
//d.sleepHalfCycle()
|
||||
//if err := d.data.Out(); err != nil {
|
||||
// return false, err
|
||||
//}
|
||||
d.clk.Out(gpio.Low)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// sleep does a busy loop to act as fast as possible.
|
||||
func (d *Dev) sleepHalfCycle() {
|
||||
host.Nanospin(clockHalfCycle)
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
// Copyright 2016 Google Inc. 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 tm1637
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/google/pio/conn/gpio"
|
||||
"github.com/google/pio/conn/gpio/gpiotest"
|
||||
"github.com/google/pio/host"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
var clk, data gpiotest.Pin
|
||||
dev, err := New(&clk, &data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to initialize tm1637: %v", err)
|
||||
}
|
||||
if _, err := dev.Write(Clock(12, 00, true)); err != nil {
|
||||
log.Fatalf("failed to write to tm1637: %v", err)
|
||||
}
|
||||
// TODO(maruel): Check the state of the pins. That's hard since it has to
|
||||
// emulate the quasi-I²C protocol.
|
||||
}
|
||||
|
||||
func Example() {
|
||||
if _, err := host.Init(); err != nil {
|
||||
log.Fatalf("failed to initialize pio: %v", err)
|
||||
}
|
||||
dev, err := New(gpio.ByNumber(6), gpio.ByNumber(12))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize tm1637: %v", err)
|
||||
}
|
||||
if err := dev.SetBrightness(Brightness10); err != nil {
|
||||
log.Fatalf("failed to set brightness on tm1637: %v", err)
|
||||
}
|
||||
if _, err := dev.Write(Clock(12, 00, true)); err != nil {
|
||||
log.Fatalf("failed to write to tm1637: %v", err)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
# experimental/devices
|
||||
|
||||
You are welcome to send PR (pull request) to add experimental device drivers
|
||||
here. Please follow the instructions in
|
||||
[CONTRIBUTING.md](../../doc/drivers/CONTRIBUTING.md).
|
||||
@ -0,0 +1,8 @@
|
||||
// Copyright 2016 Google Inc. 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 bitbang implements conn by banging on the bits (GPIO pins).
|
||||
//
|
||||
// This is not efficient but works around broken or missing drivers.
|
||||
package bitbang
|
||||
@ -0,0 +1,257 @@
|
||||
// Copyright 2016 Google Inc. 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.
|
||||
|
||||
// Specification
|
||||
//
|
||||
// http://www.nxp.com/documents/user_manual/UM10204.pdf
|
||||
|
||||
package bitbang
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/pio/conn/gpio"
|
||||
"github.com/google/pio/conn/i2c"
|
||||
"github.com/google/pio/host"
|
||||
)
|
||||
|
||||
// Use SkipAddr to skip the address from being sent.
|
||||
const SkipAddr uint16 = 0xFFFF
|
||||
|
||||
// I2C represents an I²C master implemented as bit-banging on 2 GPIO pins.
|
||||
type I2C struct {
|
||||
lock sync.Mutex
|
||||
scl gpio.PinIO // Clock line
|
||||
sda gpio.PinIO // Data line
|
||||
halfCycle time.Duration
|
||||
}
|
||||
|
||||
func (i *I2C) String() string {
|
||||
return fmt.Sprintf("bitbang/i2c(%s, %s)", i.scl, i.sda)
|
||||
}
|
||||
|
||||
// Close implements i2c.ConnCloser.
|
||||
func (i *I2C) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tx implements i2c.Conn.
|
||||
func (i *I2C) Tx(addr uint16, w, r []byte) error {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
//syscall.Setpriority(which, who, prio)
|
||||
|
||||
i.start()
|
||||
defer i.stop()
|
||||
if addr != SkipAddr {
|
||||
if addr > 0xFF {
|
||||
// Page 15, section 3.1.11 10-bit addressing
|
||||
// TOOD(maruel): Implement if desired; prefix 0b11110xx.
|
||||
return errors.New("invalid address")
|
||||
}
|
||||
// Page 13, section 3.1.10 The slave address and R/W bit
|
||||
addr <<= 1
|
||||
if len(r) == 0 {
|
||||
addr |= 1
|
||||
}
|
||||
ack, err := i.writeByte(byte(addr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ack {
|
||||
return errors.New("i2c: got NACK")
|
||||
}
|
||||
}
|
||||
for _, b := range w {
|
||||
ack, err := i.writeByte(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ack {
|
||||
return errors.New("i2c: got NACK")
|
||||
}
|
||||
}
|
||||
for x := range r {
|
||||
var err error
|
||||
r[x], err = i.readByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Speed implements i2c.Conn.
|
||||
func (i *I2C) Speed(hz int64) error {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
i.halfCycle = time.Second / time.Duration(hz) / time.Duration(2)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SCL implements i2c.Pins.
|
||||
func (i *I2C) SCL() gpio.PinIO {
|
||||
return i.scl
|
||||
}
|
||||
|
||||
// SDA implements i2c.Pins.
|
||||
func (i *I2C) SDA() gpio.PinIO {
|
||||
return i.sda
|
||||
}
|
||||
|
||||
// New returns an object that communicates I²C over two pins.
|
||||
//
|
||||
// BUG(maruel): It is close to working but not yet, the signal is incorrect
|
||||
// during ACK.
|
||||
//
|
||||
// It has two special features:
|
||||
// - Special address SkipAddr can be used to skip the address from being
|
||||
// communicated
|
||||
// - An arbitrary speed can be used
|
||||
func New(clk gpio.PinIO, data gpio.PinIO, speedHz int) (*I2C, error) {
|
||||
// Spec calls to idle at high. Page 8, section 3.1.1.
|
||||
// Set SCL as pull-up.
|
||||
if err := clk.In(gpio.Up, gpio.None); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := clk.Out(gpio.High); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set SDA as pull-up.
|
||||
if err := data.In(gpio.Up, gpio.None); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := data.Out(gpio.High); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := &I2C{
|
||||
scl: clk,
|
||||
sda: data,
|
||||
halfCycle: time.Second / time.Duration(speedHz) / time.Duration(2),
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// "When CLK is a high level and DIO changes from high to low level, data input
|
||||
// starts."
|
||||
//
|
||||
// Ends with SDA and SCL low.
|
||||
//
|
||||
// Lasts 1/2 cycle.
|
||||
func (i *I2C) start() {
|
||||
// Page 9, section 3.1.4 START and STOP conditions
|
||||
// In multi-master mode, it would have to sense SDA first and after the sleep.
|
||||
i.sda.Out(gpio.Low)
|
||||
i.sleepHalfCycle()
|
||||
i.scl.Out(gpio.Low)
|
||||
}
|
||||
|
||||
// "When CLK is a high level and DIO changes from low level to high level, data
|
||||
// input ends."
|
||||
//
|
||||
// Lasts 3/2 cycle.
|
||||
func (i *I2C) stop() {
|
||||
// Page 9, section 3.1.4 START and STOP conditions
|
||||
i.scl.Out(gpio.Low)
|
||||
i.sleepHalfCycle()
|
||||
i.scl.Out(gpio.High)
|
||||
i.sleepHalfCycle()
|
||||
i.sda.Out(gpio.High)
|
||||
// TODO(maruel): This sleep could be skipped, assuming we wait for the next
|
||||
// transfer if too quick to happen.
|
||||
i.sleepHalfCycle()
|
||||
}
|
||||
|
||||
// writeByte writes 8 bits then waits for ACK.
|
||||
//
|
||||
// Expects SDA and SCL low.
|
||||
//
|
||||
// Ends with SDA low and SCL high.
|
||||
//
|
||||
// Lasts 9 cycles.
|
||||
func (i *I2C) writeByte(b byte) (bool, error) {
|
||||
// Page 9, section 3.1.3 Data validity
|
||||
// "The data on te SDA line must be stable during the high period of the
|
||||
// clock."
|
||||
// Page 10, section 3.1.5 Byte format
|
||||
for x := 0; x < 8; x++ {
|
||||
i.sda.Out(b&byte(1<<byte(7-x)) != 0)
|
||||
i.sleepHalfCycle()
|
||||
// Let the device read SDA.
|
||||
// TODO(maruel): Support clock stretching, the device may keep the line low.
|
||||
i.scl.Out(gpio.High)
|
||||
i.sleepHalfCycle()
|
||||
i.scl.Out(gpio.Low)
|
||||
}
|
||||
// Page 10, section 3.1.6 ACK and NACK
|
||||
// 9th clock is ACK.
|
||||
i.sleepHalfCycle()
|
||||
// SCL was already set as pull-up. PullNoChange
|
||||
if err := i.scl.In(gpio.Up, gpio.None); err != nil {
|
||||
return false, err
|
||||
}
|
||||
// SDA was already set as pull-up.
|
||||
if err := i.sda.In(gpio.Up, gpio.None); err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Implement clock stretching, the device may keep the line low.
|
||||
for i.scl.Read() == gpio.Low {
|
||||
i.sleepHalfCycle()
|
||||
}
|
||||
// ACK == Low.
|
||||
ack := i.sda.Read() == gpio.Low
|
||||
if err := i.scl.Out(gpio.Low); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := i.sda.Out(gpio.Low); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ack, nil
|
||||
}
|
||||
|
||||
// readByte reads 8 bits and an ACK.
|
||||
//
|
||||
// Expects SDA and SCL low.
|
||||
//
|
||||
// Ends with SDA low and SCL high.
|
||||
//
|
||||
// Lasts 9 cycles.
|
||||
func (i *I2C) readByte() (byte, error) {
|
||||
var b byte
|
||||
if err := i.sda.In(gpio.Up, gpio.None); err != nil {
|
||||
return b, err
|
||||
}
|
||||
for x := 0; x < 8; x++ {
|
||||
i.sleepHalfCycle()
|
||||
// TODO(maruel): Support clock stretching, the device may keep the line low.
|
||||
i.scl.Out(gpio.High)
|
||||
i.sleepHalfCycle()
|
||||
if i.sda.Read() == gpio.High {
|
||||
b |= byte(1) << byte(7-x)
|
||||
}
|
||||
i.scl.Out(gpio.Low)
|
||||
}
|
||||
if err := i.sda.Out(gpio.Low); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i.sleepHalfCycle()
|
||||
i.scl.Out(gpio.High)
|
||||
i.sleepHalfCycle()
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// sleep does a busy loop to act as fast as possible.
|
||||
func (i *I2C) sleepHalfCycle() {
|
||||
host.Nanospin(i.halfCycle)
|
||||
}
|
||||
|
||||
var _ i2c.Conn = &I2C{}
|
||||
@ -0,0 +1,169 @@
|
||||
// Copyright 2016 Google Inc. 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.
|
||||
|
||||
// Specification
|
||||
//
|
||||
// Motorola never published a proper specification.
|
||||
// http://electronics.stackexchange.com/questions/30096/spi-specifications
|
||||
// http://www.nxp.com/files/microcontrollers/doc/data_sheet/M68HC11E.pdf page 120
|
||||
// http://www.st.com/content/ccc/resource/technical/document/technical_note/58/17/ad/50/fa/c9/48/07/DM00054618.pdf/files/DM00054618.pdf/jcr:content/translations/en.DM00054618.pdf
|
||||
|
||||
package bitbang
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/pio/conn/gpio"
|
||||
"github.com/google/pio/conn/spi"
|
||||
"github.com/google/pio/host"
|
||||
)
|
||||
|
||||
// SPI represents a SPI master implemented as bit-banging on 3 or 4 GPIO pins.
|
||||
type SPI struct {
|
||||
sck gpio.PinOut // Clock
|
||||
sdi gpio.PinIn // MISO
|
||||
sdo gpio.PinOut // MOSI
|
||||
csn gpio.PinOut // CS
|
||||
lock sync.Mutex
|
||||
mode spi.Mode
|
||||
bits int
|
||||
halfCycle time.Duration
|
||||
}
|
||||
|
||||
func (s *SPI) String() string {
|
||||
return fmt.Sprintf("bitbang/spi(%s, %s, %s, %s)", s.sck, s.sdi, s.sdo, s.csn)
|
||||
}
|
||||
|
||||
// Close implements spi.ConnCloser.
|
||||
func (s *SPI) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Speed implements spi.Conn.
|
||||
func (s *SPI) Speed(hz int64) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.halfCycle = time.Second / time.Duration(hz) / time.Duration(2)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Configure implements spi.Conn.
|
||||
func (s *SPI) Configure(mode spi.Mode, bits int) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if mode != spi.Mode3 {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
s.mode = mode
|
||||
s.bits = bits
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tx implements spi.Conn.
|
||||
//
|
||||
// BUG(maruel): Implement mode.
|
||||
// BUG(maruel): Implement bits.
|
||||
// BUG(maruel): Test if read works.
|
||||
func (s *SPI) Tx(w, r []byte) error {
|
||||
if len(r) != 0 && len(w) != len(r) {
|
||||
return errors.New("write and read buffers must be the same length")
|
||||
}
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if s.csn != nil {
|
||||
s.csn.Out(gpio.Low)
|
||||
s.sleepHalfCycle()
|
||||
}
|
||||
for i := uint(0); i < uint(len(w)*8); i++ {
|
||||
s.sdo.Out(w[i/8]&(1<<(i%8)) != 0)
|
||||
s.sleepHalfCycle()
|
||||
s.sck.Out(gpio.Low)
|
||||
s.sleepHalfCycle()
|
||||
if len(r) != 0 {
|
||||
if s.sdi.Read() == gpio.High {
|
||||
r[i/8] |= 1 << (i % 8)
|
||||
}
|
||||
}
|
||||
s.sck.Out(gpio.Low)
|
||||
}
|
||||
if s.csn != nil {
|
||||
s.csn.Out(gpio.High)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write implements spi.Conn.
|
||||
func (s *SPI) Write(d []byte) (int, error) {
|
||||
if err := s.Tx(d, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(d), nil
|
||||
}
|
||||
|
||||
// CLK implements spi.Pins.
|
||||
func (s *SPI) CLK() gpio.PinOut {
|
||||
return s.sck
|
||||
}
|
||||
|
||||
// MOSI implements spi.Pins.
|
||||
func (s *SPI) MOSI() gpio.PinOut {
|
||||
return s.sdo
|
||||
}
|
||||
|
||||
// MISO implements spi.Pins.
|
||||
func (s *SPI) MISO() gpio.PinIn {
|
||||
return s.sdi
|
||||
}
|
||||
|
||||
// CS implements spi.Pins.
|
||||
func (s *SPI) CS() gpio.PinOut {
|
||||
return s.csn
|
||||
}
|
||||
|
||||
// NewSPI returns an object that communicates SPI over 3 or 4 pins.
|
||||
//
|
||||
// BUG(maruel): Completely untested.
|
||||
//
|
||||
// cs can be nil.
|
||||
func NewSPI(clk, mosi gpio.PinOut, miso gpio.PinIn, cs gpio.PinOut, speedHz int64) (*SPI, error) {
|
||||
if err := clk.Out(gpio.High); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mosi.Out(gpio.High); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if miso != nil {
|
||||
if err := miso.In(gpio.Up, gpio.None); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if cs != nil {
|
||||
// Low means active.
|
||||
if err := cs.Out(gpio.High); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
s := &SPI{
|
||||
sck: clk,
|
||||
sdi: miso,
|
||||
sdo: mosi,
|
||||
csn: cs,
|
||||
mode: spi.Mode3,
|
||||
bits: 8,
|
||||
halfCycle: time.Second / time.Duration(speedHz) / time.Duration(2),
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// sleep does a busy loop to act as fast as possible.
|
||||
func (s *SPI) sleepHalfCycle() {
|
||||
host.Nanospin(s.halfCycle)
|
||||
}
|
||||
|
||||
var _ spi.Conn = &SPI{}
|
||||
@ -0,0 +1,75 @@
|
||||
// Copyright 2016 Google Inc. 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 piblaster implements interfacing code is piblaster.
|
||||
//
|
||||
// See https://github.com/sarfata/pi-blaster for more details. This package
|
||||
// relies on pi-blaster being installed and enabled.
|
||||
//
|
||||
// TODO(maruel): "dtoverlay=pwm" or "dtoverlay=pwm-2chan" works without having
|
||||
// to install anything, albeit with less pins supported.
|
||||
//
|
||||
// Warning
|
||||
//
|
||||
// piblaster doesn't report what pins is controls so it is easy to misuse this
|
||||
// library.
|
||||
package piblaster
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/google/pio/conn/gpio"
|
||||
)
|
||||
|
||||
// SetPWM enables and sets the PWM duty on a GPIO output pin via piblaster.
|
||||
//
|
||||
// duty must be [0, 1].
|
||||
func SetPWM(p gpio.PinIO, duty float32) error {
|
||||
if duty < 0 || duty > 1 {
|
||||
return fmt.Errorf("duty %f is invalid for blaster", duty)
|
||||
}
|
||||
err := openPiblaster()
|
||||
if err == nil {
|
||||
_, err = io.WriteString(piblasterHandle, fmt.Sprintf("%d=%f\n", p.Number(), duty))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ReleasePWM releases a GPIO output and leave it floating.
|
||||
//
|
||||
// This function must be called on process exit for each activated pin
|
||||
// otherwise the pin will stay in the state.
|
||||
func ReleasePWM(p gpio.PinIO) error {
|
||||
err := openPiblaster()
|
||||
if err == nil {
|
||||
_, err = io.WriteString(piblasterHandle, fmt.Sprintf("release %d\n", p.Number()))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
var piblasterHandle io.WriteCloser
|
||||
|
||||
func openPiblaster() error {
|
||||
if piblasterHandle == nil {
|
||||
f, err := os.OpenFile("/dev/pi-blaster", os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
piblasterHandle = f
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func closePiblaster() error {
|
||||
if piblasterHandle != nil {
|
||||
w := piblasterHandle
|
||||
piblasterHandle = nil
|
||||
return w.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in New Issue