diff --git a/java/libraries/io/build.xml b/java/libraries/io/build.xml index b13699db7..0e9e10234 100644 --- a/java/libraries/io/build.xml +++ b/java/libraries/io/build.xml @@ -29,4 +29,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/libraries/io/examples/I2CScreen/I2CScreen.pde b/java/libraries/io/examples/I2CScreen/I2CScreen.pde new file mode 100644 index 000000000..71a761598 --- /dev/null +++ b/java/libraries/io/examples/I2CScreen/I2CScreen.pde @@ -0,0 +1,18 @@ +import processing.io.*; + +// 0.96" 128x64 OLED display ("SKU 346540") +SSD1306 oled; + +void setup() { + size(128, 64); + + // the display can be set to one of these two addresses: 0x3c (default) or 0x3d + // (they might be listed as 0x7a and 0x7b on the circuit board) + oled = new SSD1306(I2C.list()[0], 0x3c); +} + +void draw() { + line(0, 0, 127, 63); + line(0, 63, 127, 0); + oled.sendImage(get()); +} diff --git a/java/libraries/io/examples/I2CScreen/SSD1306.pde b/java/libraries/io/examples/I2CScreen/SSD1306.pde new file mode 100644 index 000000000..6c9f343aa --- /dev/null +++ b/java/libraries/io/examples/I2CScreen/SSD1306.pde @@ -0,0 +1,118 @@ +import processing.io.I2C; + +// SSD1306 is a small, inexpensive 128x64 pixels monochrome OLED display +// available online as "0.96" 128x64 OLED display", SKU 346540 +// or from Adafruit +// datasheet: https://www.adafruit.com/datasheets/SSD1306.pdf + +class SSD1306 extends I2C { + int address; + + // there can be more than one device connected to the bus + // as long as they have different addresses + SSD1306(String dev, int address) { + super(dev); + this.address = address; + init(); + } + + protected void init() { + writeCommand(0xae); // turn display off + writeCommand(0xa8, 0x3f); // set multiplex ratio to the highest setting + writeCommand(0x8d, 0x14); // enable charge pump + writeCommand(0x20, 0x00); // set memory addressing mode to horizontal + writeCommand(0xd5, 0x80); // set display clock divide ratio & oscillator frequency to default + writeCommand(0xd3, 0x00); // no display offset + writeCommand(0x40 | 0x00); // set default display start line + + // use the following two lines to flip the display + writeCommand(0xa0 | 0x01); // set segment re-map + writeCommand(0xc8); // set COM output scan direction + + writeCommand(0xda, 0x12); // set COM pins hardware configuration + writeCommand(0xd9, 0xf1); // set pre-charge period to 241x DCLK + writeCommand(0xdB, 0x40); // set VCOMH deselect level + writeCommand(0xa4); // display RAM content (not all-on) + writeCommand(0xa6); // set normal (not-inverted) display + + // set this since we don't have access to the OLED's reset pins (?) + writeCommand(0x21, 0, 127); // set column address + writeCommand(0x22, 0, 7); // set page address + + writeCommand(0x81, 0xcf); // set contrast + writeCommand(0x2e); // deactivate scroll + writeCommand(0xaf); // turn display on + } + + void invert(boolean inverted) { + if (inverted) { + writeCommand(0xa7); + } else { + writeCommand(0xa6); + } + } + + void sendImage(PImage img) { + sendImage(img, 0, 0); + } + + void sendImage(PImage img, int startX, int startY) { + byte[] frame = new byte[1024]; + img.loadPixels(); + for (int y=startY; y < height && y-startY < 64; y++) { + for (int x=startX; x < width && x-startX < 128; x++) { + if (brightness(img.pixels[y*img.width+x]) < 128) { + // this isn't the normal (scanline) mapping, but 8 pixels below each other at a time + // white pixels have their bit turned on + frame[x + (y/8)*128] |= (1 << (y % 8)); + } + } + } + sendFramebuffer(frame); + } + + void sendFramebuffer(byte[] buf) { + if (buf.length != 1024) { + System.err.println("The framebuffer should be 1024 bytes long, with one bit per pixel"); + throw new IllegalArgumentException("Unexpected buffer size"); + } + + writeCommand(0x00 | 0x0); // set start address + writeCommand(0x10 | 0x0); // set higher column start address + writeCommand(0x40 | 0x0); // set start line + + // send the frame buffer as 16 byte long packets + for (int i=0; i < buf.length/16; i++) { + super.beginTransmission(address); + super.write(0x40); // indicates data write + for (int j=0; j < 16; j++) { + super.write(buf[i*16+j]); + } + super.endTransmission(); + } + } + + protected void writeCommand(int arg1) { + super.beginTransmission(address); + super.write(0x00); // indicates command write + super.write(arg1); + super.endTransmission(); + } + + protected void writeCommand(int arg1, int arg2) { + super.beginTransmission(address); + super.write(0x00); + super.write(arg1); + super.write(arg2); + super.endTransmission(); + } + + protected void writeCommand(int arg1, int arg2, int arg3) { + super.beginTransmission(address); + super.write(0x00); + super.write(arg1); + super.write(arg2); + super.write(arg3); + super.endTransmission(); + } +} diff --git a/java/libraries/io/examples/LedCounter/LedCounter.pde b/java/libraries/io/examples/LedCounter/LedCounter.pde index 4bc41e60f..3cd440b19 100644 --- a/java/libraries/io/examples/LedCounter/LedCounter.pde +++ b/java/libraries/io/examples/LedCounter/LedCounter.pde @@ -22,9 +22,9 @@ void draw() { // make the LEDs count in binary for (int i=0; i < leds.length; i++) { if ((frameCount & (1 << i)) != 0) { - leds[i].set(1.0); + leds[i].brightness(1.0); } else { - leds[i].set(0.0); + leds[i].brightness(0.0); } } println(frameCount); diff --git a/java/libraries/io/examples/SPIAnalogDigitalOOP8/MCP3008.pde b/java/libraries/io/examples/SPIAnalogDigitalOOP8/MCP3008.pde new file mode 100644 index 000000000..5ae8d9ef0 --- /dev/null +++ b/java/libraries/io/examples/SPIAnalogDigitalOOP8/MCP3008.pde @@ -0,0 +1,27 @@ +import processing.io.SPI; + +// MCP3008 is a Analog-to-Digital converter using SPI +// other than the MCP3001, this has 8 input channels +// datasheet: http://ww1.microchip.com/downloads/en/DeviceDoc/21295d.pdf + +class MCP3008 extends SPI { + + MCP3008(String dev) { + super(dev); + super.settings(500000, SPI.MSBFIRST, SPI.MODE0); + } + + float getAnalog(int channel) { + if (channel < 0 || 7 < channel) { + System.err.println("The channel needs to be from 0 to 7"); + throw new IllegalArgumentException("Unexpected channel"); + } + byte[] out = { 0, 0, 0 }; + // encode the channel number in the first byte + out[0] = (byte)(0x18 | channel); + byte[] in = super.transfer(out); + int val = ((in[1] & 0x03) << 8) | (in[2] & 0xff); + // val is between 0 and 1023 + return val/1023.0; + } +} diff --git a/java/libraries/io/examples/SPIAnalogDigitalOOP8/SPIAnalogDigitalOOP8.pde b/java/libraries/io/examples/SPIAnalogDigitalOOP8/SPIAnalogDigitalOOP8.pde new file mode 100644 index 000000000..58c496977 --- /dev/null +++ b/java/libraries/io/examples/SPIAnalogDigitalOOP8/SPIAnalogDigitalOOP8.pde @@ -0,0 +1,15 @@ +import processing.io.*; +MCP3008 adc; + +// see setup.png in the sketch folder for wiring details + +void setup() { + //printArray(SPI.list()); + adc = new MCP3008(SPI.list()[0]); +} + +void draw() { + background(adc.getAnalog(0) * 255); + fill(adc.getAnalog(1) * 255); + ellipse(width/2, height/2, width * 0.75, width * 0.75); +} diff --git a/java/libraries/io/examples/SimpleResistorSensor/SimpleResistorSensor.pde b/java/libraries/io/examples/SimpleResistorSensor/SimpleResistorSensor.pde new file mode 100644 index 000000000..874b699e9 --- /dev/null +++ b/java/libraries/io/examples/SimpleResistorSensor/SimpleResistorSensor.pde @@ -0,0 +1,56 @@ +import processing.io.*; + +// using a capacitor that gets charged and discharged, while +// measuring the time it takes, is an inexpensive way to +// read the value of an (analog) resistive sensor, such as +// a photocell +// kudos to ladyada for the original tutorial + +// see setup.png in the sketch folder for wiring details + +int max = 0; +int min = 9999; + +void setup() { +} + +void draw() { + int val = sensorRead(4); + println(val); + + // track largest and smallest reading, to get a sense + // how we compare + if (max < val) { + max = val; + } + if (val < min) { + min = val; + } + + // convert current reading into a number between 0.0 and 1.0 + float frac = map(val, min, max, 0.0, 1.0); + + background(255 * frac); +} + +int sensorRead(int pin) { + // discharge the capacitor + GPIO.pinMode(pin, GPIO.OUTPUT); + GPIO.digitalWrite(pin, GPIO.LOW); + delay(100); + // now the capacitor should be empty + + // measure the time takes to fill it + // up to ~ 1.4V again + GPIO.pinMode(pin, GPIO.INPUT); + int start = millis(); + while (GPIO.digitalRead(pin) == GPIO.LOW) { + // wait + } + + // return the time elapsed + // this will vary based on the value of the + // resistive sensor (lower resistance will + // make the capacitor charge faster) + return millis() - start; +} diff --git a/java/libraries/io/library.properties b/java/libraries/io/library.properties index 6bf755571..519c34193 100644 --- a/java/libraries/io/library.properties +++ b/java/libraries/io/library.properties @@ -1,2 +1,9 @@ name = Hardware I/O +authors = The Processing Foundation +url = http://processing.org/reference/libraries/io/index.html +categories = Hardware +sentence = Access peripherals on the Raspberry Pi and other Linux-based computers. +paragraph = For other platforms, this is solely provided in order to build and export sketches that require processing.io. version = 1 +prettyVersion = 1 +minRevision = 247 diff --git a/java/libraries/io/src/processing/io/GPIO.java b/java/libraries/io/src/processing/io/GPIO.java index f8b972785..a0b970955 100644 --- a/java/libraries/io/src/processing/io/GPIO.java +++ b/java/libraries/io/src/processing/io/GPIO.java @@ -177,6 +177,10 @@ public class GPIO { public static int digitalRead(int pin) { checkValidPin(pin); + if (NativeInterface.isSimulated()) { + return LOW; + } + String fn = String.format("/sys/class/gpio/gpio%d/value", pin); byte in[] = new byte[2]; int ret = NativeInterface.readFile(fn, in); @@ -222,6 +226,10 @@ public class GPIO { throw new IllegalArgumentException("Illegal value"); } + if (NativeInterface.isSimulated()) { + return; + } + String fn = String.format("/sys/class/gpio/gpio%d/value", pin); int ret = NativeInterface.writeFile(fn, out); if (ret < 0) { @@ -278,6 +286,10 @@ public class GPIO { throw new IllegalArgumentException("Unknown mode"); } + if (NativeInterface.isSimulated()) { + return; + } + String fn = String.format("/sys/class/gpio/gpio%d/edge", pin); int ret = NativeInterface.writeFile(fn, out); if (ret < 0) { @@ -325,6 +337,10 @@ public class GPIO { public static void pinMode(int pin, int mode) { checkValidPin(pin); + if (NativeInterface.isSimulated()) { + return; + } + // export pin through sysfs String fn = "/sys/class/gpio/export"; int ret = NativeInterface.writeFile(fn, Integer.toString(pin)); @@ -409,6 +425,10 @@ public class GPIO { public static void releasePin(int pin) { checkValidPin(pin); + if (NativeInterface.isSimulated()) { + return; + } + String fn = "/sys/class/gpio/unexport"; int ret = NativeInterface.writeFile(fn, Integer.toString(pin)); if (ret < 0) { @@ -451,6 +471,14 @@ public class GPIO { protected static boolean waitForInterrupt(int pin, int timeout) { checkValidPin(pin); + if (NativeInterface.isSimulated()) { + // pretend the interrupt happens after 200ms + try { + Thread.sleep(200); + } catch (InterruptedException e) {} + return true; + } + String fn = String.format("/sys/class/gpio/gpio%d/value", pin); int ret = NativeInterface.pollDevice(fn, timeout); if (ret < 0) { diff --git a/java/libraries/io/src/processing/io/I2C.java b/java/libraries/io/src/processing/io/I2C.java index 96b62767b..1d0bfac31 100644 --- a/java/libraries/io/src/processing/io/I2C.java +++ b/java/libraries/io/src/processing/io/I2C.java @@ -50,6 +50,11 @@ public class I2C { public I2C(String dev) { NativeInterface.loadLibrary(); this.dev = dev; + + if (NativeInterface.isSimulated()) { + return; + } + handle = NativeInterface.openDevice("/dev/" + dev); if (handle < 0) { throw new RuntimeException(NativeInterface.getError(handle)); @@ -81,6 +86,10 @@ public class I2C { * @webref */ public void close() { + if (NativeInterface.isSimulated()) { + return; + } + NativeInterface.closeDevice(handle); handle = 0; } @@ -107,6 +116,10 @@ public class I2C { return; } + if (NativeInterface.isSimulated()) { + return; + } + // implement these flags if needed: https://github.com/raspberrypi/linux/blob/rpi-patches/Documentation/i2c/i2c-protocol int ret = NativeInterface.transferI2c(handle, slave, out, null); transmitting = false; @@ -126,6 +139,11 @@ public class I2C { * @webref */ public static String[] list() { + if (NativeInterface.isSimulated()) { + // as on the Raspberry Pi + return new String[]{ "i2c-1" }; + } + ArrayList devs = new ArrayList(); File dir = new File("/dev"); File[] files = dir.listFiles(); @@ -159,6 +177,10 @@ public class I2C { byte[] in = new byte[len]; + if (NativeInterface.isSimulated()) { + return in; + } + int ret = NativeInterface.transferI2c(handle, slave, out, in); transmitting = false; out = null; @@ -211,18 +233,30 @@ public class I2C { /** * Adds a byte to be written to the attached device - * @param out single byte to be written (0-255) + * @param out single byte to be written, e.g. numeric literal (0 to 255, or -128 to 127) * @see beginTransmission * @see read * @see endTransmission */ public void write(int out) { - if (out < 0 || 255 < out) { - System.err.println("The write function can only operate on a single byte at a time. Call it with a value from 0 to 255."); + if (out < -128 || 255 < out) { + System.err.println("The write function can only operate on a single byte at a time. Call it with a value from 0 to 255, or -128 to 127."); throw new RuntimeException("Argument does not fit into a single byte"); } byte[] tmp = new byte[1]; tmp[0] = (byte)out; write(tmp); } + + /** + * Adds a byte to be written to the attached device + * @param out single byte to be written + * @see beginTransmission + * @see read + * @see endTransmission + */ + public void write(byte out) { + // cast to (unsigned) int + write(out & 0xff); + } } diff --git a/java/libraries/io/src/processing/io/LED.java b/java/libraries/io/src/processing/io/LED.java index 8114e58ee..61635f4d0 100644 --- a/java/libraries/io/src/processing/io/LED.java +++ b/java/libraries/io/src/processing/io/LED.java @@ -53,6 +53,10 @@ public class LED { NativeInterface.loadLibrary(); this.dev = dev; + if (NativeInterface.isSimulated()) { + return; + } + // read maximum brightness try { Path path = Paths.get("/sys/class/leds/" + dev + "/max_brightness"); @@ -105,11 +109,16 @@ public class LED { * @webref */ public void brightness(float bright) { - String fn = "/sys/class/leds/" + dev + "/brightness"; if (bright < 0.0 || 1.0 < bright) { System.err.println("Brightness must be between 0.0 and 1.0."); throw new IllegalArgumentException("Illegal argument"); } + + if (NativeInterface.isSimulated()) { + return; + } + + String fn = "/sys/class/leds/" + dev + "/brightness"; int ret = NativeInterface.writeFile(fn, Integer.toString((int)(bright * maxBrightness))); if (ret < 0) { throw new RuntimeException(fn + ": " + NativeInterface.getError(ret)); @@ -122,6 +131,10 @@ public class LED { * @webref */ public void close() { + if (NativeInterface.isSimulated()) { + return; + } + // restore previous settings String fn = "/sys/class/leds/" + dev + "/brightness"; int ret = NativeInterface.writeFile(fn, Integer.toString(prevBrightness)); @@ -143,6 +156,11 @@ public class LED { * @webref */ public static String[] list() { + if (NativeInterface.isSimulated()) { + // as on the Raspberry Pi + return new String[]{ "led0", "led1" }; + } + ArrayList devs = new ArrayList(); File dir = new File("/sys/class/leds"); File[] files = dir.listFiles(); diff --git a/java/libraries/io/src/processing/io/NativeInterface.java b/java/libraries/io/src/processing/io/NativeInterface.java index 5cb5105ad..ea32292c0 100644 --- a/java/libraries/io/src/processing/io/NativeInterface.java +++ b/java/libraries/io/src/processing/io/NativeInterface.java @@ -26,17 +26,28 @@ package processing.io; public class NativeInterface { protected static boolean loaded = false; + protected static boolean alwaysSimulate = false; public static void loadLibrary() { if (!loaded) { - if (!"Linux".equals(System.getProperty("os.name"))) { - throw new RuntimeException("The Processing I/O library is only supported on Linux"); + if (isSimulated()) { + System.err.println("The Processing I/O library is not supported on this platform. Instead of values from actual hardware ports, your sketch will only receive stand-in values that allow you to test the remainder of its functionality."); + } else { + System.loadLibrary("processing-io"); } - System.loadLibrary("processing-io"); loaded = true; } } + public static void alwaysSimulate() { + alwaysSimulate = true; + } + + public static boolean isSimulated() { + return alwaysSimulate || + !"Linux".equals(System.getProperty("os.name")); + } + public static native int openDevice(String fn); public static native String getError(int errno); diff --git a/java/libraries/io/src/processing/io/PWM.java b/java/libraries/io/src/processing/io/PWM.java index 6ea050a11..455a659e4 100644 --- a/java/libraries/io/src/processing/io/PWM.java +++ b/java/libraries/io/src/processing/io/PWM.java @@ -57,6 +57,10 @@ public class PWM { chip = channel.substring(0, pos); this.channel = Integer.parseInt(channel.substring(pos+4)); + if (NativeInterface.isSimulated()) { + return; + } + // export channel through sysfs String fn = "/sys/class/pwm/" + chip + "/export"; int ret = NativeInterface.writeFile(fn, Integer.toString(this.channel)); @@ -89,6 +93,10 @@ public class PWM { * @webref */ public void clear() { + if (NativeInterface.isSimulated()) { + return; + } + String fn = String.format("/sys/class/pwm/%s/pwm%d/enable", chip, channel); int ret = NativeInterface.writeFile(fn, "0"); if (ret < 0) { @@ -102,6 +110,10 @@ public class PWM { * @webref */ public void close() { + if (NativeInterface.isSimulated()) { + return; + } + // XXX: implicit clear()? // XXX: also check GPIO @@ -124,6 +136,10 @@ public class PWM { * @webref */ public static String[] list() { + if (NativeInterface.isSimulated()) { + return new String[]{ "pwmchip0/pwm0", "pwmchip0/pwm1" }; + } + ArrayList devs = new ArrayList(); File dir = new File("/sys/class/pwm"); File[] chips = dir.listFiles(); @@ -155,6 +171,10 @@ public class PWM { * @webref */ public void set(int period, float duty) { + if (NativeInterface.isSimulated()) { + return; + } + // set period String fn = fn = String.format("/sys/class/pwm/%s/pwm%d/period", chip, channel); // convert to nanoseconds diff --git a/java/libraries/io/src/processing/io/SPI.java b/java/libraries/io/src/processing/io/SPI.java index 83b9b7a4c..1b55134af 100644 --- a/java/libraries/io/src/processing/io/SPI.java +++ b/java/libraries/io/src/processing/io/SPI.java @@ -78,6 +78,11 @@ public class SPI { public SPI(String dev) { NativeInterface.loadLibrary(); this.dev = dev; + + if (NativeInterface.isSimulated()) { + return; + } + handle = NativeInterface.openDevice("/dev/" + dev); if (handle < 0) { throw new RuntimeException(NativeInterface.getError(handle)); @@ -90,6 +95,10 @@ public class SPI { * @webref */ public void close() { + if (NativeInterface.isSimulated()) { + return; + } + NativeInterface.closeDevice(handle); handle = 0; } @@ -110,6 +119,11 @@ public class SPI { * @webref */ public static String[] list() { + if (NativeInterface.isSimulated()) { + // as on the Raspberry Pi + return new String[]{ "spidev0.0", "spidev0.1" }; + } + ArrayList devs = new ArrayList(); File dir = new File("/dev"); File[] files = dir.listFiles(); @@ -148,6 +162,10 @@ public class SPI { * @webref */ public byte[] transfer(byte[] out) { + if (NativeInterface.isSimulated()) { + return new byte[out.length]; + } + // track the current setting per device across multiple instances String curSettings = maxSpeed + "-" + dataOrder + "-" + mode; if (!curSettings.equals(settings.get(dev))) { @@ -182,16 +200,27 @@ public class SPI { /** * Transfers data over the SPI bus - * @param out single byte to send + * @param out single byte to send, e.g. numeric literal (0 to 255, or -128 to 127) * @return bytes read in (array is the same length as out) */ public byte[] transfer(int out) { - if (out < 0 || 255 < out) { - System.err.println("The transfer function can only operate on a single byte at a time. Call it with a value from 0 to 255."); + if (out < -128 || 255 < out) { + System.err.println("The transfer function can only operate on a single byte at a time. Call it with a value from 0 to 255, or -128 to 127."); throw new RuntimeException("Argument does not fit into a single byte"); } byte[] tmp = new byte[1]; tmp[0] = (byte)out; return transfer(tmp); } + + + /** + * Transfers data over the SPI bus + * @param out single byte to send + * @return bytes read in (array is the same length as out) + */ + public byte[] transfer(byte out) { + // cast to (unsigned) int + return transfer(out & 0xff); + } }