317 lines
11 KiB
Java
317 lines
11 KiB
Java
package li.strolch.plc.core.hw.i2c;
|
|
|
|
import static com.pi4j.wiringpi.Gpio.HIGH;
|
|
import static com.pi4j.wiringpi.Gpio.LOW;
|
|
import static li.strolch.plc.model.PlcConstants.PARAM_SIMULATED;
|
|
import static li.strolch.utils.helper.ByteHelper.asBinary;
|
|
import static li.strolch.utils.helper.ByteHelper.isBitSet;
|
|
import static li.strolch.utils.helper.ExceptionHelper.getExceptionMessageWithCauses;
|
|
import static li.strolch.utils.helper.StringHelper.toHexString;
|
|
import static li.strolch.utils.helper.StringHelper.toPrettyHexString;
|
|
|
|
import java.io.IOException;
|
|
import java.util.*;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import com.pi4j.io.gpio.*;
|
|
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
|
|
import com.pi4j.io.gpio.event.GpioPinListenerDigital;
|
|
import com.pi4j.io.i2c.I2CBus;
|
|
import com.pi4j.io.i2c.I2CDevice;
|
|
import com.pi4j.io.i2c.I2CFactory;
|
|
import com.pi4j.wiringpi.Gpio;
|
|
import li.strolch.plc.core.hw.Plc;
|
|
import li.strolch.plc.core.hw.connections.SimplePlcConnection;
|
|
import li.strolch.plc.core.hw.gpio.PlcGpioController;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
public class PCF8574InputConnection extends SimplePlcConnection {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(PCF8574InputConnection.class);
|
|
|
|
private boolean verbose;
|
|
private int i2cBusNr;
|
|
private boolean inverted;
|
|
|
|
private byte[] addresses;
|
|
private I2CDevice[] inputDevices;
|
|
private boolean[][] states;
|
|
|
|
private Map<String, int[]> positionsByAddress;
|
|
|
|
private PinPullResistance interruptResistance;
|
|
private int interruptBcmPinAddress;
|
|
private PinState interruptChangeState;
|
|
private GpioPinDigitalInput interruptGpioPin;
|
|
private Future<?> interruptFixTask;
|
|
private long lastInterrupt;
|
|
private long interruptFixes;
|
|
private boolean enableInterruptFix;
|
|
|
|
public PCF8574InputConnection(Plc plc, String id) {
|
|
super(plc, id);
|
|
}
|
|
|
|
@Override
|
|
public void initialize(Map<String, Object> parameters) {
|
|
this.simulated = parameters.containsKey(PARAM_SIMULATED) && (boolean) parameters.get(PARAM_SIMULATED);
|
|
|
|
if (!parameters.containsKey("i2cBus"))
|
|
throw new IllegalArgumentException("Missing param i2cBus");
|
|
if (!parameters.containsKey("addresses"))
|
|
throw new IllegalArgumentException("Missing param addresses");
|
|
if (!parameters.containsKey("interruptPinPullResistance"))
|
|
throw new IllegalArgumentException("Missing param interruptPinPullResistance");
|
|
if (!parameters.containsKey("interruptBcmPinAddress"))
|
|
throw new IllegalArgumentException("Missing param interruptBcmPinAddress");
|
|
|
|
this.verbose = parameters.containsKey("verbose") && (Boolean) parameters.get("verbose");
|
|
this.i2cBusNr = (int) parameters.get("i2cBus");
|
|
this.inverted = parameters.containsKey("inverted") && (boolean) parameters.get("inverted");
|
|
|
|
@SuppressWarnings("unchecked")
|
|
List<Integer> addressList = (List<Integer>) parameters.get("addresses");
|
|
this.addresses = new byte[addressList.size()];
|
|
for (int i = 0; i < addressList.size(); i++) {
|
|
this.addresses[i] = addressList.get(i).byteValue();
|
|
}
|
|
|
|
Map<String, int[]> positionsByAddress = new HashMap<>();
|
|
for (int i = 0; i < this.addresses.length; i++) {
|
|
for (int j = 0; j < 8; j++)
|
|
positionsByAddress.put(this.id + "." + i + "." + j, new int[] { i, j });
|
|
}
|
|
this.positionsByAddress = Collections.unmodifiableMap(positionsByAddress);
|
|
|
|
this.interruptResistance = PinPullResistance.valueOf((String) parameters.get("interruptPinPullResistance"));
|
|
this.interruptBcmPinAddress = (Integer) parameters.get("interruptBcmPinAddress");
|
|
this.interruptChangeState = PinState.valueOf((String) parameters.get("interruptChangeState"));
|
|
this.enableInterruptFix =
|
|
parameters.containsKey("enableInterruptFix") && (Boolean) parameters.get("enableInterruptFix");
|
|
|
|
logger.info(
|
|
"Configured " + this.id + " as PCF8574 Input on I2C addresses 0x " + toPrettyHexString(this.addresses)
|
|
+ " on BCM Pin interrupt trigger " + this.interruptBcmPinAddress);
|
|
if (this.verbose)
|
|
logger.info("Verbose enabled for connection " + this.id);
|
|
}
|
|
|
|
@Override
|
|
public boolean connect() {
|
|
if (this.simulated) {
|
|
logger.warn(this.id + ": Running SIMULATED, NOT CONNECTING!");
|
|
return super.connect();
|
|
}
|
|
|
|
if (isConnected()) {
|
|
logger.warn(this.id + ": Already connected");
|
|
return true;
|
|
}
|
|
|
|
logger.info(this.id + ": Connecting...");
|
|
|
|
// initialize
|
|
try {
|
|
I2CBus i2cBus = I2CFactory.getInstance(this.i2cBusNr);
|
|
|
|
this.inputDevices = new I2CDevice[this.addresses.length];
|
|
for (int i = 0; i < this.addresses.length; i++) {
|
|
this.inputDevices[i] = i2cBus.getDevice(this.addresses[i]);
|
|
logger.info("Connected to I2C Device " + this.id + " at 0x" + toHexString(this.addresses[i])
|
|
+ " on I2C Bus " + this.i2cBusNr);
|
|
}
|
|
|
|
} catch (Throwable e) {
|
|
handleBrokenConnection(
|
|
"Failed to connect to I2C Bus " + this.i2cBusNr + " and addresses 0x " + toPrettyHexString(
|
|
this.addresses) + ": " + getExceptionMessageWithCauses(e), e);
|
|
return false;
|
|
}
|
|
|
|
boolean ok = readInitialState();
|
|
if (!ok) {
|
|
handleBrokenConnection("Failed to read initial values from I2C Bus " + this.i2cBusNr + " and addresses 0x "
|
|
+ toPrettyHexString(this.addresses), null);
|
|
}
|
|
|
|
// register interrupt listener
|
|
try {
|
|
GpioController gpioController = PlcGpioController.getInstance();
|
|
|
|
Pin interruptPin = RaspiBcmPin.getPinByAddress(this.interruptBcmPinAddress);
|
|
if (interruptPin == null)
|
|
throw new IllegalStateException(
|
|
"RaspiBcmPin with address " + this.interruptBcmPinAddress + " does not exist!");
|
|
|
|
if (gpioController.getProvisionedPins().stream().map(GpioPin::getPin).anyMatch(interruptPin::equals))
|
|
throw new IllegalStateException("Pin " + interruptPin + " is already provisioned!");
|
|
this.interruptGpioPin = gpioController.provisionDigitalInputPin(interruptPin, this.interruptResistance);
|
|
logger.info("Provisioned GPIO Input pin " + this.interruptGpioPin + " with PinPullResistance "
|
|
+ this.interruptResistance);
|
|
this.interruptGpioPin.removeAllListeners();
|
|
this.interruptGpioPin.addListener((GpioPinListenerDigital) this::handleInterrupt);
|
|
|
|
logger.info("Registered GPIO interrupt handler for BCM " + interruptPin);
|
|
|
|
if (this.enableInterruptFix) {
|
|
this.interruptFixTask = this.plc.getExecutorPool().getScheduledExecutor("InterruptFix")
|
|
.scheduleWithFixedDelay(this::checkInterruptPin, 1, 1, TimeUnit.SECONDS);
|
|
logger.info("Enabled Interrupt Fix Task.");
|
|
}
|
|
|
|
return ok && super.connect();
|
|
|
|
} catch (Throwable e) {
|
|
handleBrokenConnection("Failed to register GPIO listener for BCM pin " + this.interruptBcmPinAddress + ": "
|
|
+ getExceptionMessageWithCauses(e), e);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void disconnect() {
|
|
if (this.simulated) {
|
|
super.disconnect();
|
|
logger.warn(this.id + ": Running SIMULATED, NOT CONNECTING!");
|
|
return;
|
|
}
|
|
|
|
if (this.interruptFixTask != null)
|
|
this.interruptFixTask.cancel(true);
|
|
|
|
if (this.interruptGpioPin != null) {
|
|
try {
|
|
this.interruptGpioPin.removeAllListeners();
|
|
PlcGpioController.getInstance().unprovisionPin(this.interruptGpioPin);
|
|
logger.info("Provisioned GPIO Input pin " + this.interruptGpioPin);
|
|
} catch (Exception e) {
|
|
logger.error("Failed to unprovision pin " + this.interruptGpioPin, e);
|
|
}
|
|
}
|
|
|
|
this.inputDevices = null;
|
|
super.disconnect();
|
|
}
|
|
|
|
private void checkInterruptPin() {
|
|
|
|
// only if we haven't had an interrupt in a while
|
|
if (this.lastInterrupt > System.currentTimeMillis() - 1000) {
|
|
return;
|
|
}
|
|
|
|
int currentState = Gpio.digitalRead(this.interruptGpioPin.getPin().getAddress());
|
|
|
|
if ((this.interruptChangeState == PinState.HIGH && currentState == HIGH) //
|
|
|| (this.interruptChangeState == PinState.LOW && currentState == LOW)) {
|
|
logger.error("Missed interrupt for pin " + this.interruptGpioPin + " as current state is " + currentState
|
|
+ " and expected change state is " + this.interruptChangeState + ", forcing update...");
|
|
|
|
try {
|
|
handleNewState("interruptFix");
|
|
} catch (Exception e) {
|
|
handleBrokenConnection("Failed to read new state: " + getExceptionMessageWithCauses(e), e);
|
|
}
|
|
|
|
this.interruptFixes++;
|
|
logger.error("Performed " + this.interruptFixes + " interrupt fixes.");
|
|
}
|
|
}
|
|
|
|
private void handleInterrupt(GpioPinDigitalStateChangeEvent event) {
|
|
if (this.verbose)
|
|
logger.info(event.getPin() + " " + event.getState() + " " + event.getEdge());
|
|
|
|
try {
|
|
if (event.getState() == this.interruptChangeState)
|
|
handleNewState("interrupt");
|
|
} catch (Exception e) {
|
|
handleBrokenConnection("Failed to read new state: " + getExceptionMessageWithCauses(e), e);
|
|
}
|
|
}
|
|
|
|
private void handleNewState(String ctx) throws IOException {
|
|
|
|
for (int i = 0; i < this.inputDevices.length; i++) {
|
|
I2CDevice i2CDevice = this.inputDevices[i];
|
|
if (i2CDevice == null) {
|
|
logger.warn("Ignoring invalid I2C Device 0x" + toHexString(this.addresses[i]) + " " + ctx);
|
|
continue;
|
|
}
|
|
|
|
byte data = (byte) i2CDevice.read();
|
|
|
|
if (this.verbose)
|
|
logger.info(
|
|
this.id + " at 0x" + toHexString((byte) i2CDevice.getAddress()) + " has new state " + asBinary(
|
|
data) + " " + ctx);
|
|
|
|
for (int j = 0; j < 8; j++) {
|
|
boolean newState = isBitSet(data, j);
|
|
if (this.inverted)
|
|
newState = !newState;
|
|
|
|
if (this.states[i][j] != newState) {
|
|
this.states[i][j] = newState;
|
|
String address = this.id + "." + i + "." + j;
|
|
logger.info("Detected " + address + " = " + (newState ? 1 : 0) + (this.inverted ?
|
|
" (inverted) " :
|
|
" (normal) ") + asBinary(data) + " " + ctx);
|
|
this.plc.queueNotify(address, newState);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.lastInterrupt = System.currentTimeMillis();
|
|
}
|
|
|
|
private boolean readInitialState() {
|
|
|
|
boolean ok = true;
|
|
|
|
this.states = new boolean[this.inputDevices.length][];
|
|
|
|
for (int i = 0; i < this.inputDevices.length; i++) {
|
|
I2CDevice i2CDevice = this.inputDevices[i];
|
|
try {
|
|
byte data = (byte) i2CDevice.read();
|
|
logger.info(
|
|
"Initial Value for " + this.id + " at 0x" + toHexString(this.addresses[i]) + " is " + asBinary(
|
|
data));
|
|
|
|
this.states[i] = new boolean[8];
|
|
for (int j = 0; j < 8; j++) {
|
|
boolean bitSet = isBitSet(data, j);
|
|
|
|
if (this.inverted)
|
|
bitSet = !bitSet;
|
|
|
|
this.states[i][j] = bitSet;
|
|
this.plc.queueNotify(this.id + "." + i + "." + j, this.states[i][j]);
|
|
}
|
|
} catch (Exception e) {
|
|
ok = false;
|
|
this.inputDevices[i] = null;
|
|
logger.error("Failed to read initial state for " + this.id + " at 0x" + toHexString(
|
|
(byte) i2CDevice.getAddress()), e);
|
|
}
|
|
}
|
|
|
|
this.lastInterrupt = System.currentTimeMillis();
|
|
return ok;
|
|
}
|
|
|
|
@Override
|
|
public Set<String> getAddresses() {
|
|
return new TreeSet<>(this.positionsByAddress.keySet());
|
|
}
|
|
|
|
@Override
|
|
public void send(String address, Object value) {
|
|
throw new UnsupportedOperationException(getClass() + " does not support output!");
|
|
}
|
|
}
|