307 lines
9.9 KiB
Java
307 lines
9.9 KiB
Java
package li.strolch.plc.core.hw;
|
|
|
|
import static java.util.stream.Collectors.toSet;
|
|
|
|
import java.util.*;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.stream.Stream;
|
|
|
|
import li.strolch.plc.model.PlcAddress;
|
|
import li.strolch.plc.model.PlcAddressKey;
|
|
import li.strolch.plc.model.PlcAddressType;
|
|
import li.strolch.utils.ExecutorPool;
|
|
import li.strolch.utils.collections.MapOfLists;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
public class DefaultPlc implements Plc {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(DefaultPlc.class);
|
|
public static final String VIRTUAL_BOOLEAN = "VirtualBoolean";
|
|
public static final String VIRTUAL_STRING = "VirtualString";
|
|
public static final String VIRTUAL_INTEGER = "VirtualInteger";
|
|
|
|
private final Map<String, PlcAddress> notificationMappings;
|
|
private final Map<String, PlcConnection> connections;
|
|
private final Map<String, PlcConnection> connectionsByAddress;
|
|
private final MapOfLists<PlcAddress, PlcListener> listeners;
|
|
|
|
private final LinkedBlockingQueue<NotificationTask> notificationTasks;
|
|
|
|
private PlcListener globalListener;
|
|
private PlcConnectionStateChangeListener connectionStateChangeListener;
|
|
private boolean verbose;
|
|
|
|
private ExecutorPool executorPool;
|
|
private Future<?> notificationsTask;
|
|
private boolean run;
|
|
|
|
public DefaultPlc() {
|
|
this.notificationMappings = new HashMap<>();
|
|
this.listeners = new MapOfLists<>(true);
|
|
this.connections = new HashMap<>();
|
|
this.connectionsByAddress = new HashMap<>();
|
|
this.notificationTasks = new LinkedBlockingQueue<>();
|
|
}
|
|
|
|
@Override
|
|
public void setVerbose(boolean verbose) {
|
|
this.verbose = verbose;
|
|
}
|
|
|
|
@Override
|
|
public void setGlobalListener(PlcListener listener) {
|
|
this.globalListener = listener;
|
|
}
|
|
|
|
@Override
|
|
public void notifyConnectionStateChanged(PlcConnection connection) {
|
|
if (this.connectionStateChangeListener != null)
|
|
this.connectionStateChangeListener.notifyStateChange(connection);
|
|
}
|
|
|
|
@Override
|
|
public void setConnectionStateChangeListener(PlcConnectionStateChangeListener listener) {
|
|
this.connectionStateChangeListener = listener;
|
|
}
|
|
|
|
@Override
|
|
public Stream<PlcAddressKey> getAddressKeysStream() {
|
|
return this.notificationMappings.values().stream().map(PlcAddress::toPlcAddressKey);
|
|
}
|
|
|
|
@Override
|
|
public Set<PlcAddressKey> getAddressKeys() {
|
|
return getAddressKeysStream().collect(toSet());
|
|
}
|
|
|
|
@Override
|
|
public void register(PlcAddress address, PlcListener listener) {
|
|
this.listeners.addElement(address, listener);
|
|
logger.info(address.toKeyAddress() + ": " + listener.getClass().getSimpleName());
|
|
}
|
|
|
|
@Override
|
|
public void unregister(PlcAddress address, PlcListener listener) {
|
|
if (this.listeners.removeElement(address, listener)) {
|
|
logger.info(address + ": " + listener.getClass().getName());
|
|
} else {
|
|
logger.warn("Listener not registered with key " + address.toKeyAddress() + ": " + listener.getClass()
|
|
.getSimpleName());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void syncNotify(String address, Object value) {
|
|
doNotify(address, value, true);
|
|
}
|
|
|
|
@Override
|
|
public void queueNotify(String address, Object value) {
|
|
this.notificationTasks.add(new NotificationTask(address, value));
|
|
}
|
|
|
|
private void doNotify(String address, Object value, boolean verbose) {
|
|
PlcAddress plcAddress = this.notificationMappings.get(address);
|
|
if (plcAddress == null) {
|
|
logger.warn("No mapping to PlcAddress for hwAddress " + address);
|
|
return;
|
|
}
|
|
|
|
if (plcAddress.inverted) {
|
|
if (value instanceof Boolean)
|
|
value = !((boolean) value);
|
|
else
|
|
logger.error(plcAddress + " is marked as inverted, but the value is not a boolean, but a "
|
|
+ value.getClass());
|
|
}
|
|
|
|
doNotify(plcAddress, value, verbose, true, true);
|
|
}
|
|
|
|
private void doNotify(PlcAddress plcAddress, Object value, boolean verbose, boolean catchExceptions,
|
|
boolean notifyGlobalListener) {
|
|
|
|
List<PlcListener> listeners = this.listeners.getList(plcAddress);
|
|
if (listeners == null || listeners.isEmpty()) {
|
|
logger.warn("No listener for update {}: {}", plcAddress.toKey(), value);
|
|
} else {
|
|
listeners = new ArrayList<>(listeners);
|
|
for (PlcListener listener : listeners) {
|
|
try {
|
|
if (this.verbose)
|
|
logger.info("Notifying " + plcAddress.toKey() + ": " + value + " @ " + listener);
|
|
listener.handleNotification(plcAddress, value);
|
|
} catch (Exception e) {
|
|
if (catchExceptions) {
|
|
logger.error("Failed to notify listener " + listener + " for address " + plcAddress, e);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (notifyGlobalListener && this.globalListener != null)
|
|
this.globalListener.handleNotification(plcAddress, value);
|
|
}
|
|
|
|
private void doNotifications() {
|
|
logger.info("Notifications Task running...");
|
|
while (this.run) {
|
|
NotificationTask task = null;
|
|
try {
|
|
task = this.notificationTasks.take();
|
|
doNotify(task.address, task.value, this.verbose);
|
|
} catch (InterruptedException e) {
|
|
logger.error("Interrupted!");
|
|
} catch (Exception e) {
|
|
if (task != null)
|
|
logger.error("Failed to perform notification for " + task.address + ": " + task.value, e);
|
|
else
|
|
logger.error("Failed to get notification task", e);
|
|
}
|
|
}
|
|
logger.info("Notifications Task stopped.");
|
|
}
|
|
|
|
@Override
|
|
public void send(PlcAddress plcAddress) {
|
|
send(plcAddress, true, true);
|
|
}
|
|
|
|
@Override
|
|
public void send(PlcAddress plcAddress, Object value) {
|
|
send(plcAddress, value, true, true);
|
|
}
|
|
|
|
@Override
|
|
public void send(PlcAddress plcAddress, boolean catchExceptions, boolean notifyGlobalListener) {
|
|
logger.info("Sending {}: {} (default)", plcAddress.toKey(), plcAddress.defaultValue);
|
|
if (!isVirtual(plcAddress))
|
|
validateConnection(plcAddress).send(plcAddress.address, plcAddress.defaultValue);
|
|
doNotify(plcAddress, plcAddress.defaultValue, false, catchExceptions, notifyGlobalListener);
|
|
}
|
|
|
|
@Override
|
|
public void send(PlcAddress plcAddress, Object value, boolean catchExceptions, boolean notifyGlobalListener) {
|
|
logger.info("Sending {}: {}", plcAddress.toKey(), value);
|
|
if (!isVirtual(plcAddress))
|
|
validateConnection(plcAddress).send(plcAddress.address, value);
|
|
doNotify(plcAddress, value, false, catchExceptions, notifyGlobalListener);
|
|
}
|
|
|
|
private PlcConnection validateConnection(PlcAddress plcAddress) {
|
|
PlcConnection connection = getConnection(plcAddress);
|
|
if (!connection.isAutoConnect() || connection.isConnected())
|
|
return connection;
|
|
|
|
connection.connect();
|
|
|
|
if (connection.isConnected())
|
|
return connection;
|
|
|
|
throw new IllegalStateException(
|
|
"Could not connect to " + connection.getId() + " due to " + connection.getStateMsg());
|
|
}
|
|
|
|
@Override
|
|
public void addConnection(PlcConnection connection) {
|
|
this.connections.put(connection.getId(), connection);
|
|
Set<String> addresses = connection.getAddresses();
|
|
logger.info("Adding connection " + connection.getId() + " " + connection.getClass().getName() + " with "
|
|
+ addresses.size() + " addresses...");
|
|
for (String address : addresses) {
|
|
logger.info(" Adding " + address + "...");
|
|
this.connectionsByAddress.put(address, connection);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void start() {
|
|
this.executorPool = new ExecutorPool();
|
|
this.run = true;
|
|
this.notificationsTask = this.executorPool.getSingleThreadExecutor("PlcNotify").submit(this::doNotifications);
|
|
this.connections.values().stream().filter(PlcConnection::isAutoConnect).forEach(PlcConnection::connect);
|
|
}
|
|
|
|
@Override
|
|
public void stop() {
|
|
this.run = false;
|
|
if (this.notificationsTask != null)
|
|
this.notificationsTask.cancel(true);
|
|
this.connections.values().forEach(PlcConnection::disconnect);
|
|
if (this.executorPool != null)
|
|
this.executorPool.destroy();
|
|
}
|
|
|
|
@Override
|
|
public PlcConnection getConnection(PlcAddress address) {
|
|
PlcConnection plcConnection = this.connectionsByAddress.get(address.address);
|
|
if (plcConnection == null)
|
|
throw new IllegalStateException("No PlcConnection exists for " + address.toKeyAddress());
|
|
return plcConnection;
|
|
}
|
|
|
|
@Override
|
|
public PlcConnection getConnection(String id) {
|
|
PlcConnection plcConnection = this.connections.get(id);
|
|
if (plcConnection == null)
|
|
throw new IllegalStateException("No PlcConnection exists with id " + id);
|
|
return plcConnection;
|
|
}
|
|
|
|
@Override
|
|
public void registerNotificationMapping(PlcAddress address) {
|
|
|
|
boolean virtual = isVirtual(address);
|
|
if (virtual)
|
|
validateVirtualAddress(address);
|
|
else if (!this.connectionsByAddress.containsKey(address.address))
|
|
throw new IllegalStateException("No PlcConnection exists for " + address.toKeyAddress());
|
|
|
|
if (address.type != PlcAddressType.Notification)
|
|
throw new IllegalArgumentException("Key must be of type " + PlcAddressType.Notification + ": " + address);
|
|
|
|
PlcAddress replaced = this.notificationMappings.put(address.address, address);
|
|
if (replaced != null)
|
|
throw new IllegalArgumentException(
|
|
"Replaced mapping for address " + address.address + " for key " + replaced + " with " + address);
|
|
|
|
logger.info("Registered " + address);
|
|
}
|
|
|
|
private void validateVirtualAddress(PlcAddress address) {
|
|
|
|
if (address.address.equals(VIRTUAL_BOOLEAN) || address.address.equals(VIRTUAL_BOOLEAN + ".")) {
|
|
throw new IllegalStateException(
|
|
"Virtual address " + address.address + " is missing sub component for " + address);
|
|
}
|
|
|
|
if (address.address.equals(VIRTUAL_STRING) || address.address.equals(VIRTUAL_STRING + ".")) {
|
|
throw new IllegalStateException(
|
|
"Virtual address " + address.address + " is missing sub component for " + address);
|
|
}
|
|
|
|
if (address.address.equals(VIRTUAL_INTEGER) || address.address.equals(VIRTUAL_INTEGER + ".")) {
|
|
throw new IllegalStateException(
|
|
"Virtual address " + address.address + " is missing sub component for " + address);
|
|
}
|
|
}
|
|
|
|
private boolean isVirtual(PlcAddress address) {
|
|
return address.address.startsWith(VIRTUAL_BOOLEAN) //
|
|
|| address.address.startsWith(VIRTUAL_STRING) //
|
|
|| address.address.startsWith(VIRTUAL_INTEGER);
|
|
}
|
|
|
|
@Override
|
|
public ExecutorPool getExecutorPool() {
|
|
return this.executorPool;
|
|
}
|
|
|
|
private record NotificationTask(String address, Object value) {
|
|
}
|
|
}
|