com.diozero.internal.provider.voodoospark.VoodooSparkDeviceFactory Maven / Gradle / Ivy
The newest version!
package com.diozero.internal.provider.voodoospark;
/*-
* #%L
* Organisation: diozero
* Project: diozero - Voodoo Spark provider for Particle Photon
* Filename: VoodooSparkDeviceFactory.java
*
* This file is part of the diozero project. More information about this project
* can be found at https://www.diozero.com/.
* %%
* Copyright (C) 2016 - 2024 diozero
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import org.tinylog.Logger;
import com.diozero.api.AbstractDigitalInputDevice;
import com.diozero.api.DeviceMode;
import com.diozero.api.DigitalInputEvent;
import com.diozero.api.GpioEventTrigger;
import com.diozero.api.GpioPullUpDown;
import com.diozero.api.I2CConstants;
import com.diozero.api.PinInfo;
import com.diozero.api.RuntimeIOException;
import com.diozero.api.SerialDevice;
import com.diozero.api.SpiClockMode;
import com.diozero.internal.spi.AnalogInputDeviceInterface;
import com.diozero.internal.spi.AnalogOutputDeviceInterface;
import com.diozero.internal.spi.BaseNativeDeviceFactory;
import com.diozero.internal.spi.GpioDigitalInputDeviceInterface;
import com.diozero.internal.spi.GpioDigitalInputOutputDeviceInterface;
import com.diozero.internal.spi.GpioDigitalOutputDeviceInterface;
import com.diozero.internal.spi.InternalI2CDeviceInterface;
import com.diozero.internal.spi.InternalPwmOutputDeviceInterface;
import com.diozero.internal.spi.InternalSerialDeviceInterface;
import com.diozero.internal.spi.InternalServoDeviceInterface;
import com.diozero.internal.spi.InternalSpiDeviceInterface;
import com.diozero.sbc.BoardInfo;
import com.diozero.sbc.LocalSystemInfo;
import com.diozero.util.PropertyUtil;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToByteEncoder;
public class VoodooSparkDeviceFactory extends BaseNativeDeviceFactory {
public static final String DEVICE_NAME = "VoodooSpark";
private static final String DEVICE_ID_PROP = "PARTICLE_DEVICE_ID";
private static final String ACCESS_TOKEN_PROP = "PARTICLE_TOKEN";
static final int MAX_ANALOG_VALUE = (int) (Math.pow(2, 12) - 1);
static final int MAX_PWM_VALUE = (int) (Math.pow(2, 8) - 1);
private static final int DEFAULT_FREQUENCY = 500;
// Voodoo Spark network commands
private static final byte PIN_MODE = 0x00;
private static final byte DIGITAL_WRITE = 0x01;
private static final byte ANALOG_WRITE = 0x02;
private static final byte DIGITAL_READ = 0x03;
private static final byte ANALOG_READ = 0x04;
private static final byte REPORTING = 0x05;
private static final byte SET_SAMPLE_INTERVAL = 0x06;
private static final byte INTERNAL_RGB = 0x07;
private static final byte PING_READ = 0x08;
/* NOTE GAP */
// private static final byte SERIAL_BEGIN = 0x10;
// private static final byte SERIAL_END = 0x11;
// private static final byte SERIAL_PEEK = 0x12;
// private static final byte SERIAL_AVAILABLE = 0x13;
// private static final byte SERIAL_WRITE = 0x14;
// private static final byte SERIAL_READ = 0x15;
// private static final byte SERIAL_FLUSH = 0x16;
/* NOTE GAP */
// private static final byte SPI_BEGIN = 0x20;
// private static final byte SPI_END = 0x21;
// private static final byte SPI_SET_BIT_ORDER = 0x22;
// private static final byte SPI_SET_CLOCK = 0x23;
// private static final byte SPI_SET_DATA_MODE = 0x24;
// private static final byte SPI_TRANSFER = 0x25;
// /* NOTE GAP */
private static final byte I2C_CONFIG = 0x30;
private static final byte I2C_WRITE = 0x31;
private static final byte I2C_READ = 0x32;
private static final byte I2C_READ_CONTINUOUS = 0x33;
private static final byte I2C_REGISTER_NOT_SPECIFIED = (byte) 0xFF;
/* NOTE GAP */
private static final byte SERVO_WRITE = 0x41;
private static final byte ACTION_RANGE = 0x46;
private Queue messageQueue;
private EventLoopGroup workerGroup;
private Channel messageChannel;
private Lock lock;
private Condition condition;
private ChannelFuture lastWriteFuture;
private int timeoutMs;
public VoodooSparkDeviceFactory() {
String device_id = PropertyUtil.getProperty(DEVICE_ID_PROP, null);
String access_token = PropertyUtil.getProperty(ACCESS_TOKEN_PROP, null);
if (device_id == null || access_token == null) {
Logger.error("Both {} and {} properties must be set", DEVICE_ID_PROP, ACCESS_TOKEN_PROP);
}
timeoutMs = 2000;
messageQueue = new LinkedList<>();
lock = new ReentrantLock();
condition = lock.newCondition();
// Lookup the local IP address using the Particle "endpoint" custom variable
try {
URL url = new URL(String.format("https://api.particle.io/v1/devices/%s/endpoint?access_token=%s", device_id,
URLEncoder.encode(access_token, StandardCharsets.UTF_8.name())));
Endpoint endpoint = new Gson().fromJson(new InputStreamReader(url.openStream()), Endpoint.class);
Logger.debug(endpoint);
String[] ip_port = endpoint.result.split(":");
connect(ip_port[0], Integer.parseInt(ip_port[1]));
} catch (IOException | NumberFormatException | InterruptedException e) {
// 403 - device id not found
// 401 - bad access token
Logger.error(e, "Error: {}", e);
throw new RuntimeIOException("Error getting local endpoint", e);
}
}
private void connect(String host, int port) throws InterruptedException {
workerGroup = new NioEventLoopGroup();
ResponseHandler rh = new ResponseHandler(this::messageReceived);
Bootstrap b1 = new Bootstrap();
b1.group(workerGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ResponseDecoder(), new MessageEncoder(), rh);
}
});
// Connect
messageChannel = b1.connect(host, port).sync().channel();
}
@Override
public void shutdown() {
if (messageChannel == null || !messageChannel.isOpen()) {
return;
}
messageChannel.close();
try {
messageChannel.closeFuture().sync();
// Wait until all messages are flushed before closing the channel.
if (lastWriteFuture != null) {
lastWriteFuture.sync();
}
} catch (InterruptedException e) {
System.err.println("Error: " + e);
e.printStackTrace(System.err);
} finally {
workerGroup.shutdownGracefully();
}
}
@Override
public String getName() {
return DEVICE_NAME;
}
@Override
protected BoardInfo lookupBoardInfo() {
return new ParticlePhotonBoardInfo();
}
@Override
public int getBoardPwmFrequency() {
return DEFAULT_FREQUENCY;
}
@Override
public void setBoardPwmFrequency(int frequency) {
// Ignore
Logger.warn("Not implemented");
}
@Override
public int getBoardServoFrequency() {
return 50;
}
@Override
public void setBoardServoFrequency(int frequency) {
// Ignore
Logger.warn("Not implemented");
}
@Override
public DeviceMode getGpioMode(int gpio) {
return DeviceMode.UNKNOWN;
}
@Override
public int getGpioValue(int gpio) {
return getValue(gpio) ? 1 : 0;
}
@Override
public GpioDigitalInputDeviceInterface createDigitalInputDevice(String key, PinInfo pinInfo, GpioPullUpDown pud,
GpioEventTrigger trigger) {
return new VoodooSparkDigitalInputDevice(this, key, pinInfo, pud, trigger);
}
@Override
public GpioDigitalOutputDeviceInterface createDigitalOutputDevice(String key, PinInfo pinInfo,
boolean initialValue) {
return new VoodooSparkDigitalOutputDevice(this, key, pinInfo, initialValue);
}
@Override
public GpioDigitalInputOutputDeviceInterface createDigitalInputOutputDevice(String key, PinInfo pinInfo,
DeviceMode mode) {
return new VoodooSparkDigitalInputOutputDevice(this, key, pinInfo, mode);
}
@Override
public InternalPwmOutputDeviceInterface createPwmOutputDevice(String key, PinInfo pinInfo, int pwmFrequency,
float initialValue) {
Logger.warn("PWM frequency will be ignored - Firmata does not allow this to be specified");
return new VoodooSparkPwmOutputDevice(this, key, pinInfo, pwmFrequency, initialValue);
}
@Override
public InternalServoDeviceInterface createServoDevice(String key, PinInfo pinInfo, int frequency,
int minPulseWidthUs, int maxPulseWidthUs, int initialPulseWidthUs) {
throw new UnsupportedOperationException("Not currently implemented");
}
@Override
public AnalogInputDeviceInterface createAnalogInputDevice(String key, PinInfo pinInfo) {
return new VoodooSparkAnalogInputDevice(this, key, pinInfo);
}
@Override
public AnalogOutputDeviceInterface createAnalogOutputDevice(String key, PinInfo pinInfo, float initialValue) {
return new VoodooSparkAnalogOutputDevice(this, key, pinInfo, initialValue);
}
@Override
public InternalSpiDeviceInterface createSpiDevice(String key, int controller, int chipSelect, int frequency,
SpiClockMode spiClockMode, boolean lsbFirst) throws RuntimeIOException {
throw new UnsupportedOperationException("SPI isn't supported with Voodoo Spark firmware");
}
@Override
public InternalI2CDeviceInterface createI2CDevice(String key, int controller, int address,
I2CConstants.AddressSize addressSize) throws RuntimeIOException {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public InternalSerialDeviceInterface createSerialDevice(String key, String deviceFile, int baud,
SerialDevice.DataBits dataBits, SerialDevice.StopBits stopBits, SerialDevice.Parity parity,
boolean readBlocking, int minReadChars, int readTimeoutMillis) throws RuntimeIOException {
throw new UnsupportedOperationException("Not yet implemented");
}
void setPinMode(int gpio, PinMode mode) {
sendMessage(new PinModeMessage(gpio, mode));
}
boolean getValue(int gpio) {
ResponseMessage response = sendMessage(new DigitalReadMessage(gpio));
if (response == null) {
return false;
}
// Validate GPIO returned matches that requested
if (response.pinOrPort != gpio) {
Logger.error("Returned GPIO ({}) doesn't match that requested ({})", Byte.valueOf(response.pinOrPort),
Integer.valueOf(gpio));
}
return response.lsb != 0;
}
void setValue(int gpio, boolean value) {
sendMessage(new DigitalWriteMessage(gpio, value));
}
int getAnalogValue(int gpio) {
ResponseMessage response = sendMessage(new AnalogReadMessage(gpio));
if (response == null) {
return -1;
}
// Validate GPIO returned matches that requested
if (response.pinOrPort != gpio) {
Logger.error("Returned GPIO ({}) doesn't match that requested ({})", Byte.valueOf(response.pinOrPort),
Integer.valueOf(gpio));
}
return (response.msb << 7) | response.lsb;
}
void setAnalogValue(int gpio, int value) {
sendMessage(new AnalogWriteMessage(gpio, value));
}
void addReporting(int gpio, boolean analog) {
sendMessage(new ReportingMessage((byte) gpio, analog));
}
void setSampleInterval(int intervalMs) {
sendMessage(new SetSampleIntervalMessage(intervalMs));
}
void setInternalRgb(byte red, byte green, byte blue) {
sendMessage(new InternalRgbMessage(red, green, blue));
}
private synchronized ResponseMessage sendMessage(Message message) {
ResponseMessage rm = null;
lock.lock();
try {
lastWriteFuture = messageChannel.writeAndFlush(message);
lastWriteFuture.get();
if (message.responseExpected) {
if (condition.await(timeoutMs, TimeUnit.MILLISECONDS)) {
rm = messageQueue.remove();
if (rm.cmd != message.cmd) {
throw new RuntimeIOException(
"Unexpected response: " + rm.cmd + ", was expecting " + message.cmd + "; discarding");
}
} else {
throw new RuntimeIOException("Timeout waiting for response to command " + message.cmd);
}
}
} catch (ExecutionException e) {
throw new RuntimeIOException(e);
} catch (InterruptedException e) {
Logger.error(e, "Interrupted: {}", e);
} finally {
lock.unlock();
}
return rm;
}
void messageReceived(ResponseMessage msg) {
if (msg.cmd == REPORTING) {
long epoch_time = System.currentTimeMillis();
Logger.info("Reporting message: {}", msg);
// Notify the listeners for each GPIO in this port for which reporting has been
// enabled
for (int i = 0; i < 8; i++) {
// Note can only get reports for GPIOs 0-7 and 10-17
int gpio = msg.pinOrPort * 10 + i;
// TODO Need to check that reporting has been enabled for this GPIO!
PinInfo pin_info = getBoardPinInfo().getByGpioNumberOrThrow(gpio);
AbstractDigitalInputDevice input_device = getDevice(createPinKey(pin_info));
if (input_device != null) {
// What about analog events?!
input_device.accept(new DigitalInputEvent(gpio, epoch_time, 0, (msg.lsb & (1 << i)) != 0));
}
}
} else {
lock.lock();
try {
messageQueue.add(msg);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
private static final class Endpoint {
String cmd;
String name;
String result;
CoreInfo coreInfo;
@Override
public String toString() {
return "Endpoint [cmd=" + cmd + ", name=" + name + ", result=" + result + ", coreInfo=" + coreInfo + "]";
}
}
private static final class CoreInfo {
@SerializedName("last_app")
String lastApp;
@SerializedName("last_heard")
Date lastHeard;
boolean connected;
@SerializedName("last_handshake_at")
Date lastHandshakeAt;
@SerializedName("deviceID")
String deviceId;
@SerializedName("product_id")
int productId;
@Override
public String toString() {
return "CoreInfo [lastApp=" + lastApp + ", lastHeard=" + lastHeard + ", connected=" + connected
+ ", lastHandshakeAt=" + lastHandshakeAt + ", deviceId=" + deviceId + ", productId=" + productId
+ "]";
}
}
// Classes to support Netty encode / decode
static final class MessageEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
out.writeBytes(msg.encode());
}
}
static final class ResponseDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List