commit eb384b9d0cdfeb0181e04418f9b9183fab45d3c6 Author: Marc-Antoine Ruel Date: Wed Sep 28 15:00:37 2016 -0400 Initial commit of pio. This was extracted from github.com/maruel/dlibox/go/pio @ f51102b. This code was written by Marc-Antoine Ruel. diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..1c4cf86 --- /dev/null +++ b/AUTHORS @@ -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. diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..3fd7127 --- /dev/null +++ b/CONTRIBUTORS @@ -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 +# Individual's name +# +# 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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4f2bc1 --- /dev/null +++ b/README.md @@ -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 + [![GoDoc](https://godoc.org/github.com/google/pio?status.svg)](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. diff --git a/devices/apa102/apa102.go b/devices/apa102/apa102.go new file mode 100644 index 0000000..1cf30d4 --- /dev/null +++ b/devices/apa102/apa102.go @@ -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{} diff --git a/devices/apa102/apa102_test.go b/devices/apa102/apa102_test.go new file mode 100644 index 0000000..20c72c1 --- /dev/null +++ b/devices/apa102/apa102_test.go @@ -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) + } +} diff --git a/devices/apa102/doc.go b/devices/apa102/doc.go new file mode 100644 index 0000000..9dd9ef4 --- /dev/null +++ b/devices/apa102/doc.go @@ -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 diff --git a/devices/bme280/bme280.go b/devices/bme280/bme280.go new file mode 100644 index 0000000..1912e83 --- /dev/null +++ b/devices/bme280/bme280.go @@ -0,0 +1,394 @@ +// 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 controls a Bosch BME280 device over I²C. +// +// Datasheet +// +// https://cdn-shop.adafruit.com/datasheets/BST-BME280_DS001-10.pdf +package bme280 + +import ( + "errors" + + "github.com/google/pio/conn" + "github.com/google/pio/conn/i2c" + "github.com/google/pio/conn/spi" + "github.com/google/pio/devices" +) + +// Oversampling affects how much time is taken to measure each of temperature, +// pressure and humidity. +// +// Temperature must be measured for pressure and humidity to be measured. The +// duration is approximatively: +// duration_in_ms = 1 + 2*temp + 2*press+0.5 + 2*humidy+0.5 +// +// Using high oversampling and low standby results in highest power +// consumption, but this is still below 1mA so we generally don't care. +type Oversampling uint8 + +// Possible oversampling values. +const ( + No Oversampling = 0 + O1x Oversampling = 1 + O2x Oversampling = 2 + O4x Oversampling = 3 + O8x Oversampling = 4 + O16x Oversampling = 5 +) + +// Standby is the time the BME280 waits idle between measurements. This reduces +// power consumption when the host won't read the values as fast as the +// measurements are done. +type Standby uint8 + +// Possible standby values, these determines the refresh rate. +const ( + S500us Standby = 0 + S10ms Standby = 6 + S20ms Standby = 7 + S62ms Standby = 1 + S125ms Standby = 2 + S250ms Standby = 3 + S500ms Standby = 4 + S1s Standby = 5 +) + +// Filter specifies the internal IIR filter to get steady measurements without +// using oversampling. This is mainly used to reduce power consumption. +type Filter uint8 + +// Possible filtering values. +const ( + FOff Filter = 0 + F2 Filter = 1 + F4 Filter = 2 + F8 Filter = 3 + F16 Filter = 4 +) + +// Dev is an handle to a bme280. +type Dev struct { + d conn.Conn + isSPI bool + c calibration +} + +// Sense returns measurements as °C, kPa and % of relative humidity. +func (d *Dev) Sense(env *devices.Environment) error { + // All registers must be read in a single pass, as noted at page 21, section + // 4.1. + // Pressure: 0xF7~0xF9 + // Temperature: 0xFA~0xFC + // Humidity: 0xFD~0xFE + buf := [0xFF - 0xF7]byte{} + if err := d.readReg(0xF7, buf[:]); err != nil { + return err + } + // These values are 20 bits as per doc. + pRaw := int32(buf[0])<<12 | int32(buf[1])<<4 | int32(buf[2])>>4 + tRaw := int32(buf[3])<<12 | int32(buf[4])<<4 | int32(buf[5])>>4 + // This value is 16 bits as per doc. + hRaw := int32(buf[6])<<8 | int32(buf[7]) + + t, tFine := d.c.compensateTempInt(tRaw) + env.Temperature = devices.Celcius(t * 10) + + p := d.c.compensatePressureInt64(pRaw, tFine) + env.Pressure = devices.KPascal((int32(p) + 127) / 256) + + h := d.c.compensateHumidityInt(hRaw, tFine) + env.Humidity = devices.RelativeHumidity((int32(h)*100 + 511) / 1024) + return nil +} + +// Stop stops the bme280 from acquiring measurements. It is recommended to call +// to reduce idle power usage. +func (d *Dev) Stop() error { + // Page 27 (for register) and 12~13 section 3.3. + return d.writeCommands([]byte{0xF4, byte(sleep)}) +} + +// Opts is optional options to pass to the constructor. +// +// Recommended (and default) values are O4x for oversampling, S20ms for standby +// and FOff for filter if planing to call frequently, else use S500ms to get a +// bit more than one reading per second. +// +// BUG(maruel): Remove the Standby flag and replace with a +// WaitForNextSample(time.Duration). Then use the closest value automatically. +type Opts struct { + Temperature Oversampling + Pressure Oversampling + Humidity Oversampling + Standby Standby + Filter Filter +} + +// NewI2C returns an object that communicates over I²C to BME280 environmental +// sensor. +// +// It is recommended to call Stop() when done with the device so it stops +// sampling. +func NewI2C(i i2c.Conn, opts *Opts) (*Dev, error) { + d := &Dev{d: &i2c.Dev{Conn: i, Addr: 0x76}, isSPI: false} + if err := d.makeDev(opts); err != nil { + return nil, err + } + return d, nil +} + +// NewSPI returns an object that communicates over SPI to BME280 environmental +// sensor. +// +// Recommended values are O4x for oversampling, S20ms for standby and FOff for +// filter if planing to call frequently, else use S500ms to get a bit more than +// one reading per second. +// +// It is recommended to call Stop() when done with the device so it stops +// sampling. +// +// When using SPI, the CS line must be used. +// +// BUG(maruel): This code was not tested yet, still waiting for a SPI enabled +// device in the mail. +func NewSPI(s spi.Conn, opts *Opts) (*Dev, error) { + // It works both in Mode0 and Mode3. + if err := s.Configure(spi.Mode3, 8); err != nil { + return nil, err + } + d := &Dev{d: s, isSPI: true} + if err := d.makeDev(opts); err != nil { + return nil, err + } + return d, nil +} + +// + +// mode is stored in config +type mode byte + +const ( + sleep mode = 0 // no operation, all registers accessible, lowest power, selected after startup + forced mode = 1 // perform one measurement, store results and return to sleep mode + normal mode = 3 // perpetual cycling of measurements and inactive periods +) + +type status byte + +const ( + measuring status = 8 // set when conversion is running + imUpdate status = 1 // set when NVM data are being copied to image registers +) + +var defaults = Opts{ + Temperature: O4x, + Pressure: O4x, + Humidity: O4x, + Standby: S20ms, + Filter: FOff, +} + +func (d *Dev) makeDev(opts *Opts) error { + if opts == nil { + opts = &defaults + } + config := []byte{ + // ctrl_meas; put it to sleep otherwise the config update may be ignored. + 0xF4, byte(opts.Temperature)<<5 | byte(opts.Pressure)<<2 | byte(sleep), + // ctrl_hum + 0xF2, byte(opts.Humidity), + // config + 0xF5, byte(opts.Standby)<<5 | byte(opts.Filter)<<2, + // ctrl_meas + 0xF4, byte(opts.Temperature)<<5 | byte(opts.Pressure)<<2 | byte(normal), + } + + // The device starts in 2ms as per datasheet. No need to wait for boot to be + // finished. + + var chipID [1]byte + // Read register 0xD0 to read the chip id. + if err := d.readReg(0xD0, chipID[:]); err != nil { + return err + } + if chipID[0] != 0x60 { + return errors.New("unexpected chip id; is this a BME280?") + } + // Read calibration data t1~3, p1~9, 8bits padding, h1. + var tph [0xA2 - 0x88]byte + if err := d.readReg(0x88, tph[:]); err != nil { + return err + } + // Read calibration data h2~6 + var h [0xE8 - 0xE1]byte + if err := d.readReg(0xE1, h[:]); err != nil { + return err + } + if err := d.writeCommands(config[:]); err != nil { + return err + } + + d.c.t1 = uint16(tph[0]) | uint16(tph[1])<<8 + d.c.t2 = int16(tph[2]) | int16(tph[3])<<8 + d.c.t3 = int16(tph[4]) | int16(tph[5])<<8 + d.c.p1 = uint16(tph[6]) | uint16(tph[7])<<8 + d.c.p2 = int16(tph[8]) | int16(tph[9])<<8 + d.c.p3 = int16(tph[10]) | int16(tph[11])<<8 + d.c.p4 = int16(tph[12]) | int16(tph[13])<<8 + d.c.p5 = int16(tph[14]) | int16(tph[15])<<8 + d.c.p6 = int16(tph[16]) | int16(tph[17])<<8 + d.c.p7 = int16(tph[18]) | int16(tph[19])<<8 + d.c.p8 = int16(tph[20]) | int16(tph[21])<<8 + d.c.p9 = int16(tph[22]) | int16(tph[23])<<8 + d.c.h1 = uint8(tph[25]) + + d.c.h2 = int16(h[0]) | int16(h[1])<<8 + d.c.h3 = uint8(h[2]) + d.c.h4 = int16(h[3])<<4 | int16(h[4])&0xF + d.c.h5 = int16(h[4])>>4 | int16(h[5])<<4 + d.c.h6 = int8(h[6]) + return nil +} + +func (d *Dev) readReg(reg uint8, b []byte) error { + // Page 32-33 + if d.isSPI { + read := make([]byte, len(b)+1) + write := make([]byte, len(read)) + write[0] = reg + if err := d.d.Tx(write, read); err != nil { + return err + } + copy(b, read[:1]) + } + return d.d.Tx([]byte{reg}, b) +} + +// writeCommands writes a command to the bme280. +// +// Warning: b may be modified! +func (d *Dev) writeCommands(b []byte) error { + if d.isSPI { + // Page 33; set RW bit 7 to 0. + for i := 0; i < len(b); i += 2 { + b[i] &^= 0x80 + } + } + _, err := d.d.Write(b) + return err +} + +// Register table: +// 0x00..0x87 -- +// 0x88..0xA1 Calibration data +// 0xA2..0xCF -- +// 0xD0 Chip id; reads as 0x60 +// 0xD1..0xDF -- +// 0xE0 Reset by writing 0xB6 to it +// 0xE1..0xF0 Calibration data +// 0xF1 -- +// 0xF2 ctrl_hum; ctrl_meas must be writen to after for change to this register to take effect +// 0xF3 status +// 0xF4 ctrl_meas +// 0xF5 config +// 0xF6 -- +// 0xF7 press_msb +// 0xF8 press_lsb +// 0xF9 press_xlsb +// 0xFA temp_msb +// 0xFB temp_lsb +// 0xFC temp_xlsb +// 0xFD hum_msb +// 0xFE hum_lsb + +// https://cdn-shop.adafruit.com/datasheets/BST-BME280_DS001-10.pdf +// Page 23 + +type calibration struct { + t1 uint16 + t2, t3 int16 + p1 uint16 + p2, p3, p4, p5, p6, p7, p8, p9 int16 + h2 int16 // Reordered for packing + h1, h3 uint8 + h4, h5 int16 + h6 int8 +} + +// Pages 23-24 + +// compensateTempInt returns temperature in °C, resolution is 0.01 °C. +// Output value of 5123 equals 51.23 C. +// +// raw has 20 bits of resolution. +func (c *calibration) compensateTempInt(raw int32) (int32, int32) { + x := ((raw>>3 - int32(c.t1)<<1) * int32(c.t2)) >> 11 + y := ((((raw>>4 - int32(c.t1)) * (raw>>4 - int32(c.t1))) >> 12) * int32(c.t3)) >> 14 + tFine := x + y + return (tFine*5 + 128) >> 8, tFine +} + +// compensatePressureInt64 returns pressure in Pa in Q24.8 format (24 integer +// bits and 8 fractional bits). Output value of 24674867 represents +// 24674867/256 = 96386.2 Pa = 963.862 hPa. +// +// raw has 20 bits of resolution. +func (c *calibration) compensatePressureInt64(raw, tFine int32) uint32 { + x := int64(tFine) - 128000 + y := x * x * int64(c.p6) + y += (x * int64(c.p5)) << 17 + y += int64(c.p4) << 35 + x = (x*x*int64(c.p3))>>8 + ((x * int64(c.p2)) << 12) + x = ((int64(1)<<47 + x) * int64(c.p1)) >> 33 + if x == 0 { + return 0 + } + p := ((((1048576 - int64(raw)) << 31) - y) * 3125) / x + x = (int64(c.p9) * (p >> 13) * (p >> 13)) >> 25 + y = (int64(c.p8) * p) >> 19 + return uint32(((p + x + y) >> 8) + (int64(c.p7) << 4)) +} + +// compensateHumidityInt returns humidity in %RH in Q22.10 format (22 integer +// and 10 fractional bits). Output value of 47445 represents 47445/1024 = +// 46.333% +// +// raw has 16 bits of resolution. +func (c *calibration) compensateHumidityInt(raw, tFine int32) uint32 { + x := tFine - 76800 + /* + Yes, someone wrote the following in the datasheet unironically: + v_x1_u32r = (((((adc_H << 14) – (((BME280_S32_t)dig_H4) << 20) – + (((BME280_S32_t)dig_H5) * v_x1_u32r)) + ((BME280_S32_t)16384)) >> 15) * + (((((((v_x1_u32r * ((BME280_S32_t)dig_H6)) >> 10) * (((v_x1_u32r * + ((BME280_S32_t)dig_H3)) >> 11) + ((BME280_S32_t)32768))) >> 10) + + ((BME280_S32_t)2097152)) * ((BME280_S32_t)dig_H2) + 8192) >> 14)); + + v_x1_u32r = (v_x1_u32r – (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((BME280_S32_t)dig_H1)) >> 4)); + v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r); + v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r); + */ + // Here's a more "readable" version: + x1 := raw<<14 - int32(c.h4)<<20 - int32(c.h5)*x + x2 := (x1 + 16384) >> 15 + x3 := (x * int32(c.h6)) >> 10 + x4 := (x * int32(c.h3)) >> 11 + x5 := (x3 * (x4 + 32768)) >> 10 + x6 := ((x5+2097152)*int32(c.h2) + 8192) >> 14 + x = x2 * x6 + + x = x - ((((x>>15)*(x>>15))>>7)*int32(c.h1))>>4 + if x < 0 { + return 0 + } + if x > 419430400 { + return 419430400 >> 12 + } + return uint32(x >> 12) +} + +var _ devices.Environmental = &Dev{} diff --git a/devices/bme280/bme280_test.go b/devices/bme280/bme280_test.go new file mode 100644 index 0000000..73b21f0 --- /dev/null +++ b/devices/bme280/bme280_test.go @@ -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) +} diff --git a/devices/devices.go b/devices/devices.go new file mode 100644 index 0000000..62b2acb --- /dev/null +++ b/devices/devices.go @@ -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 +} diff --git a/devices/devices_test.go b/devices/devices_test.go new file mode 100644 index 0000000..37f9232 --- /dev/null +++ b/devices/devices_test.go @@ -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) + } +} diff --git a/devices/devicestest/display.go b/devices/devicestest/display.go new file mode 100644 index 0000000..f89a65b --- /dev/null +++ b/devices/devicestest/display.go @@ -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{} diff --git a/devices/devicestest/doc.go b/devices/devicestest/doc.go new file mode 100644 index 0000000..a6f588c --- /dev/null +++ b/devices/devicestest/doc.go @@ -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 diff --git a/devices/doc.go b/devices/doc.go new file mode 100644 index 0000000..3bc73db --- /dev/null +++ b/devices/doc.go @@ -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 diff --git a/devices/lirc/doc.go b/devices/lirc/doc.go new file mode 100644 index 0000000..395affa --- /dev/null +++ b/devices/lirc/doc.go @@ -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 '' /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 diff --git a/devices/lirc/lirc.go b/devices/lirc/lirc.go new file mode 100644 index 0000000..c4958c0 --- /dev/null +++ b/devices/lirc/lirc.go @@ -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: