
net.freeutils.scrollphat.Device Maven / Gradle / Ivy
/*
* Copyright © 2016 Amichai Rothman
*
* This file is part of JScrollPhat - the Java Scroll pHAT package.
*
* JScrollPhat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* JScrollPhat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JScrollPhat. If not, see .
*
* For additional info see http://www.freeutils.net/source/jscrollphat/
*/
package net.freeutils.scrollphat;
import java.io.Closeable;
import java.io.IOException;
/**
* Provides access to the raw functionality of
* ISSI's IS31FL3730 matrix LED driver chip.
*
* The device must be {@link #open opened} and {@link #configure configured}
* before being used (the {@link #init} method does both with default values).
*
* The device must be {@link #close closed} when done, in order to clear the
* display and release all resources. The {@link #closeOnShutdown method} can
* be called to enable a shutdown hook that will close the device automatically
* when the JVM exits, which is convenient in most simple use cases.
*
* The LED matrix data is written via the {@link #setDisplay} methods. Data is
* expressed as a single byte per column, where each bit represents the state
* of a single led on that column. Columns are indexed from left to right, and
* the rows within a column are indexed from top (lowest bit) to bottom (highest bit).
*
* The written matrix data is stored by the device in temporary registers, and
* becomes visible only when the {@link #update} method is invoked to apply the
* changes.
*
* The LED brightness can be set as a value in the range 0 (off) to 128 (brightest).
* The brightness setting applies to the entire matrix and cannot be set for
* each LED individually.
*
* This class is not thread-safe, and should only be accessed by one thread
* at a time.
*
* @see IS31FL3730 Datasheet
*/
public abstract class Device implements Closeable {
// IS31FL3730 I2C addresses (according to how the address pin is connected)
public static final int
ADDR_GND = 0x60, // address pin connected to GND
ADDR_SCL = 0x61, // address pin connected to SCL
ADDR_SDA = 0x62, // address pin connected to SDA
ADDR_VCC = 0x63; // address pin connected to VCC
// IS31FL3730 registers
public static final byte
REG_CONFIG = 0x00,
REG_MATRIX1_DATA_START = 0x01,
REG_MATRIX2_DATA_START = 0x0e,
REG_UPDATE = 0x0c,
REG_BRIGHTNESS_CONFIG = 0x0d,
REG_BRIGHTNESS_PWM = 0x19,
REG_RESET = (byte)0xff;
// IS31FL3730 configuration options (bitmask)
public static final byte
CONFIG_MATRIX_8X8 = 0x00,
CONFIG_MATRIX_7X9 = 0x01,
CONFIG_MATRIX_6X10 = 0x02,
CONFIG_MATRIX_5X11 = 0x03,
CONFIG_AUDIO_INPUT = 0x04,
CONFIG_DISPLAY_MATRIX_1 = 0x00,
CONFIG_DISPLAY_MATRIX_2 = 0x08,
CONFIG_DISPLAY_MATRIX_1_AND_2 = 0x18,
CONFIG_SOFTWARE_SHUTDOWN = (byte)0x80;
protected int errors;
protected int warnErrorCount = 10;
protected int throwErrorCount = 1000;
protected int width;
protected int height;
protected Thread shutdownHook;
/**
* Returns a new Device instance.
*
* The implementation name can be specified in the "scrollphat.impl"
* system property, and if none exists, a default is used.
*
* @return a new Device instance
* @throws IllegalArgumentException if the implementation name is invalid
*/
public static Device newInstance() {
return newInstance(null);
}
/**
* Returns a new Device instance using the given implementation.
*
* The implementation name can be one of "pi4j", "jnadev" or "mock".
* If null, the implementation name is taken from the "scrollphat.impl"
* system property, and if none exists, a default is used.
*
* @param impl the implementation name
* @return a new Device instance
* @throws IllegalArgumentException if the implementation name is invalid
*/
public static Device newInstance(String impl) {
if (impl == null)
impl = System.getProperty("scrollphat.impl", "pi4j");
impl = impl.toLowerCase();
if (impl.equals("pi4j"))
return new Pi4JDevice();
if (impl.equals("jnadev"))
return new JNADevDevice();
if (impl.equals("mock"))
return new MockDevice();
throw new IllegalArgumentException("unknown device implementation: " + impl);
}
/**
* Returns the configured device width.
*
* @return the configured device width
*/
public int getWidth() {
return width;
}
/**
* Returns the configured device height.
*
* @return the configured device height
*/
public int getHeight() {
return height;
}
/**
* Handles errors that occur while writing data to the device.
*
* The error may be rethrown, swallowed, or a warning printed,
* depending on the error handling configuration and current
* accumulated error count.
*
* @param ioe the error that occurred
* @throws IOException if the error is to be propagated
*/
protected void handleError(IOException ioe) throws IOException {
errors++;
if (warnErrorCount > 0 && errors % warnErrorCount == 0)
System.err.println("warning: accumulated " + errors +
" errors, check your connections (last error: " + ioe + ")");
if (throwErrorCount > 0 && errors % throwErrorCount == 0)
throw ioe;
}
/**
* Opens the connection to I2C bus number 1
* and to the device at the given address.
*
* @param address the I2C device address
* @return this device
* @throws IOException if an error occurs
* @throws IllegalArgumentException if the address is invalid
*/
public Device open(int address) throws IOException {
return open(1, address);
}
/**
* Opens the connection to the I2C bus and
* to the device at the given address.
*
* @param busNumber the I2C bus number to use
* @param address the I2C device address
* @return this device
* @throws IOException if an error occurs
* @throws IllegalArgumentException if the address is invalid
*/
public Device open(int busNumber, int address) throws IOException {
if (busNumber < 0)
throw new IllegalArgumentException("invalid bus number: " + busNumber);
if (address < ADDR_GND || address > ADDR_VCC)
throw new IllegalArgumentException("invalid address: " + address);
return openImpl(busNumber, address);
}
/**
* Opens the connection to the I2C bus and
* to the device at the given address.
*
* @param busNumber the I2C bus number to use
* @param address the I2C device address
* @return this device
* @throws IOException if an error occurs
* @throws IllegalArgumentException if the address is invalid
*/
protected abstract Device openImpl(int busNumber, int address) throws IOException;
/**
* Closes the I2C bus and device.
*
* This method may be called multiple times, or
* without a successful call to {@link #open} before it.
*
* @throws IOException if an error occurs
*/
public void close() throws IOException {
closeImpl();
}
/**
* Closes the I2C bus and device.
*
* This method may be called multiple times, or
* without a successful call to {@link #open} before it.
*
* @throws IOException if an error occurs
*/
protected abstract void closeImpl() throws IOException;
/**
* Writes a single byte of data to the given register.
*
* @param register the register to write to
* @param data the data to write (only the lower 8 bits are written)
* @throws IOException if an error occurs
*/
public void write(byte register, int data) throws IOException {
try {
writeImpl(register, data);
} catch (IOException ioe) {
handleError(ioe);
}
}
/**
* Writes a single byte of data to the given register.
*
* @param register the register to write to
* @param data the data to write (only the lower 8 bits are written)
* @throws IOException if an error occurs
*/
protected void writeImpl(byte register, int data) throws IOException {
write(register, new byte[] { (byte)data }, 0, 1);
}
/**
* Writes a series of bytes of data starting at the given register.
* The register number is incremented by one after each written byte.
*
* @param register the register to write to
* @param data an array containing the data to write
* @param offset the offset within the array of the first byte to write
* @param length the number of bytes to write
* @throws IOException if an error occurs
*/
public void write(byte register, byte[] data, int offset, int length) throws IOException {
try {
writeImpl(register, data, offset, length);
} catch (IOException ioe) {
handleError(ioe);
}
}
/**
* Writes a series of bytes of data starting at the given register.
* The register number is incremented by one after each written byte.
*
* @param register the register to write to
* @param data an array containing the data to write
* @param offset the offset within the array of the first byte to write
* @param length the number of bytes to write
* @throws IOException if an error occurs
*/
protected abstract void writeImpl(byte register, byte[] data, int offset, int length) throws IOException;
/**
* Specifies whether to set a system shutdown
* hook to close this device when the JVM exits.
*
* If disabled, the caller is responsible for performing a graceful
* shutdown, e.g. by calling {@link #close} in a finally block.
*
* @param enable specifies whether to enable or disable
* close on shutdown
* @return this device
*/
public Device closeOnShutdown(boolean enable) {
if (enable && shutdownHook == null) {
shutdownHook = new Thread() {
@Override
public void run() {
try {
close();
} catch (IOException ignore) {}
}
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
} else if (!enable && shutdownHook != null) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
shutdownHook = null;
}
return this;
}
/**
* Configures the device.
*
* @param config the bitmask of configuration options
* @return this device
* @throws IOException if an error occurs
*/
public Device configure(int config) throws IOException {
reset();
if ((config & ~0xff) != 0)
throw new IllegalArgumentException("invalid config bitmask: " + config);
write(REG_CONFIG, config);
this.width = 8 + (config & 3);
this.height = 16 - width;
return this;
}
/**
* Initializes the device.
*
* This is a convenience method that calls {@link #closeOnShutdown},
* {@link #open} and {@link #configure} with default options.
*
* @return this device
* @throws IOException if an error occurs
*/
public Device init() throws IOException {
closeOnShutdown(true);
return open(ADDR_GND).configure((int)CONFIG_MATRIX_5X11);
}
/**
* Resets the device (by writing the reset command to it).
*
* @throws IOException if an error occurs
*/
public void reset() throws IOException {
write(REG_RESET, 0);
}
/**
* Sets the display brightness.
*
* @param value a brightness value in the range 0-128
* @throws IOException if an error occurs
*/
public void setBrightness(int value) throws IOException {
write(REG_BRIGHTNESS_PWM, value);
}
/**
* Updates the display. This method must be called after modifying
* the display data in order for the changes to be applied.
*
* @throws IOException if an error occurs
*/
public void update() throws IOException {
write(REG_UPDATE, 0);
}
/**
* Writes the given display data and updates the display.
*
* @param columns the consecutive column values to write
* @throws IOException if an error occurs
*/
public void update(byte[] columns) throws IOException {
setDisplay(columns);
update();
}
/**
* Sets the value of the given column.
*
* @param column the column number
* @param value the column value
* @throws IOException if an error occurs
*/
public void setDisplay(int column, int value) throws IOException {
if (column < 0 || column >= width)
throw new IndexOutOfBoundsException("column " + column);
write((byte)(REG_MATRIX1_DATA_START + column), value);
}
/**
* Sets the values of a consecutive sequence of columns.
*
* @param startColumn the column to start writing at
* @param columns an array containing the consecutive column values to write
* @param offset the offset within the array of the first column's value
* @param length the number of consecutive column values to write
* @throws IOException if an error occurs
*/
public void setDisplay(int startColumn, byte[] columns, int offset, int length) throws IOException {
// clip to boundaries
if (startColumn < 0) {
offset -= startColumn;
length += startColumn;
startColumn = 0;
}
length = Math.min(length, Math.min(width - startColumn, columns.length - offset));
if (length > 0)
write((byte)(REG_MATRIX1_DATA_START + startColumn), columns, offset, length);
}
/**
* Sets the values of a consecutive sequence of columns.
*
* @param startColumn the column to start writing at
* @param columns the consecutive column values to write
* @throws IOException if an error occurs
*/
public void setDisplay(int startColumn, byte[] columns) throws IOException {
setDisplay(startColumn, columns, 0, width);
}
/**
* Sets the values of a consecutive sequence of columns,
* starting at the first column.
*
* @param columns the consecutive column values to write
* @throws IOException if an error occurs
*/
public void setDisplay(byte[] columns) throws IOException {
setDisplay(0, columns, 0, width);
}
/**
* Displays test patterns. This functionality is implemented
* in software and not by the device itself, but is useful
* in verifying that the device is working properly.
*
* @throws IOException if an error occurs
* @throws InterruptedException if the thread is interrupted
*/
public void displayTestPatterns() throws IOException, InterruptedException {
int width = getWidth();
byte[] matrix = new byte[width];
for (int round = 0; round < 4; round++) {
setBrightness(1);
for (int i = 0; i < 2 * width; i++) {
if (i == width) {
for (int j = 0, levels = 60; j < levels; j++) {
setBrightness(1 + 20 * Math.min(j, levels - j) / levels);
Thread.sleep(800 / levels);
}
}
matrix[i % width] = i < width ? (byte)0xff : 0; // turn column on/off
update(matrix);
Thread.sleep(800 / width);
}
}
}
/**
* The main command-line utility entry point.
*
* @param args the arguments
* @throws IOException if an error occurs
* @throws InterruptedException if the thread is interrupted
*/
public static void main(String[] args) throws IOException, InterruptedException {
newInstance().init().displayTestPatterns();
}
}