strolch-plc/plc-core/src/main/java/li/strolch/plc/core/DefaultPlcHandler.java

470 lines
15 KiB
Java

package li.strolch.plc.core;
import static java.lang.System.nanoTime;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static li.strolch.model.StrolchModelConstants.BAG_PARAMETERS;
import static li.strolch.plc.model.PlcConstants.*;
import static li.strolch.utils.helper.ExceptionHelper.getCallerMethod;
import static li.strolch.utils.helper.ExceptionHelper.getExceptionMessageWithCauses;
import static li.strolch.utils.helper.StringHelper.formatNanoDuration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Consumer;
import li.strolch.agent.api.ComponentContainer;
import li.strolch.agent.api.StrolchComponent;
import li.strolch.model.Locator;
import li.strolch.model.Resource;
import li.strolch.model.log.LogMessage;
import li.strolch.model.parameter.Parameter;
import li.strolch.model.parameter.StringParameter;
import li.strolch.model.visitor.SetParameterValueVisitor;
import li.strolch.persistence.api.StrolchTransaction;
import li.strolch.plc.core.hw.*;
import li.strolch.plc.core.hw.gpio.PlcGpioController;
import li.strolch.plc.model.ConnectionState;
import li.strolch.plc.model.PlcAddress;
import li.strolch.plc.model.PlcAddressType;
import li.strolch.plc.model.PlcState;
import li.strolch.privilege.model.Certificate;
import li.strolch.privilege.model.PrivilegeContext;
import li.strolch.runtime.configuration.ComponentConfiguration;
import li.strolch.utils.collections.MapOfMaps;
import li.strolch.utils.dbc.DBC;
public class DefaultPlcHandler extends StrolchComponent implements PlcHandler, PlcConnectionStateChangeListener {
public static final int SILENT_THRESHOLD = 60;
private static final int MAX_MESSAGE_QUEUE = 200;
private PrivilegeContext ctx;
private String plcId;
private Plc plc;
private PlcState plcState;
private String plcStateMsg;
private MapOfMaps<String, String, PlcAddress> plcAddresses;
private MapOfMaps<String, String, PlcAddress> plcTelegrams;
private Map<PlcAddress, String> addressesToResourceId;
private GlobalPlcListener globalListener;
private LinkedBlockingDeque<Runnable> updateStateQueue;
private LinkedBlockingDeque<Consumer<GlobalPlcListener>> messageQueue;
private boolean run;
private Future<?> messageSenderTask;
private Future<?> updateStateTask;
private boolean verbose;
public DefaultPlcHandler(ComponentContainer container, String componentName) {
super(container, componentName);
}
@Override
public ComponentContainer getContainer() {
return super.getContainer();
}
@Override
public String getPlcId() {
return this.plcId;
}
@Override
public Plc getPlc() {
return this.plc;
}
@Override
public PlcState getPlcState() {
return this.plcState;
}
@Override
public String getPlcStateMsg() {
return this.plcStateMsg;
}
@Override
public PlcAddress getPlcAddress(String resource, String action) {
DBC.PRE.assertNotNull("resource must not be null", resource);
DBC.PRE.assertNotEmpty("action must not be empty", action);
PlcAddress plcAddress = this.plcAddresses.getElement(resource, action);
if (plcAddress == null)
throw new IllegalStateException("PlcAddress for " + resource + "-" + action + " does not exist!");
return plcAddress;
}
@Override
public String getPlcAddressId(String resource, String action) {
DBC.PRE.assertNotNull("resource must not be null", resource);
DBC.PRE.assertNotEmpty("action must not be empty", action);
PlcAddress plcAddress = getPlcAddress(resource, action);
String addressId = this.addressesToResourceId.get(plcAddress);
if (addressId == null)
throw new IllegalStateException(
"PlcAddress mapping ID for " + resource + "-" + action + " does not exist!");
return addressId;
}
@Override
public void initialize(ComponentConfiguration configuration) throws Exception {
this.plcId = configuration.getString("plcId", null);
// validate Plc class name
String plcClassName = configuration.getString("plcClass", DefaultPlc.class.getName());
Class.forName(plcClassName);
this.plcState = PlcState.Initial;
this.plcStateMsg = PlcState.Initial.name();
this.plcAddresses = new MapOfMaps<>();
this.plcTelegrams = new MapOfMaps<>();
this.addressesToResourceId = new HashMap<>();
this.verbose = configuration.getBoolean("verbose", false);
this.messageQueue = new LinkedBlockingDeque<>();
this.updateStateQueue = new LinkedBlockingDeque<>();
super.initialize(configuration);
}
@Override
public void start() throws Exception {
this.ctx = getContainer().getPrivilegeHandler().openAgentSystemUserContext();
this.run = true;
this.messageSenderTask = getSingleThreadExecutor("LogSender").submit(this::sendMessages);
this.updateStateTask = getSingleThreadExecutor("UpdateState").submit(this::updateStates);
if (reconfigurePlc())
startPlc();
super.start();
}
@Override
public void stop() throws Exception {
stopPlc();
this.run = false;
if (this.messageSenderTask != null)
this.messageSenderTask.cancel(true);
if (this.updateStateTask != null)
this.updateStateTask.cancel(true);
if (this.ctx != null)
getContainer().getPrivilegeHandler().invalidate(this.ctx.getCertificate());
super.stop();
}
@Override
public void destroy() throws Exception {
if (PlcGpioController.isLoaded()) {
logger.info("Destroying GPIO Controller...");
PlcGpioController.getInstance().shutdown();
}
super.destroy();
}
@Override
public void startPlc() {
if (this.plc == null)
throw new IllegalStateException("Can not start as not yet configured!");
this.plc.start();
this.plcState = PlcState.Started;
this.plcStateMsg = PlcState.Started.name();
logger.info("Started PLC");
}
@Override
public void stopPlc() {
if (this.plc != null)
this.plc.stop();
this.plcState = PlcState.Stopped;
this.plcStateMsg = PlcState.Stopped.name();
logger.info("Stopped PLC");
}
@Override
public boolean reconfigurePlc() {
if (this.plcState == PlcState.Started)
throw new IllegalStateException("Can not reconfigure if started!");
try {
MapOfMaps<String, String, PlcAddress> plcAddresses = new MapOfMaps<>();
MapOfMaps<String, String, PlcAddress> plcTelegrams = new MapOfMaps<>();
Map<PlcAddress, String> addressesToResourceId = new HashMap<>();
this.plc = configure(validateCtx(), plcAddresses, plcTelegrams, addressesToResourceId);
this.plc.setVerbose(this.verbose);
this.plcAddresses = plcAddresses;
this.plcTelegrams = plcTelegrams;
this.addressesToResourceId = addressesToResourceId;
this.plcState = PlcState.Configured;
this.plcStateMsg = PlcState.Configured.name();
if (this.globalListener != null)
this.plc.setGlobalListener(this.globalListener);
logger.info("Reconfigured PLC with " + this.plcAddresses.size() + " addresses");
return true;
} catch (Exception e) {
logger.error("Failed to configure Plc", e);
this.plcState = PlcState.Failed;
this.plcStateMsg = "Configure failed: " + getExceptionMessageWithCauses(e);
return false;
}
}
private Plc configure(PrivilegeContext ctx, MapOfMaps<String, String, PlcAddress> plcAddresses,
MapOfMaps<String, String, PlcAddress> plcTelegrams, Map<PlcAddress, String> addressesToResourceId)
throws Exception {
Plc plc;
try (StrolchTransaction tx = openTx(ctx.getCertificate(), getCallerMethod(), true)) {
String plcClassName = getConfiguration().getString("plcClass", DefaultPlc.class.getName());
plc = PlcConfigurator.configurePlc(tx, plcClassName, plcAddresses, plcTelegrams, addressesToResourceId);
plc.setConnectionStateChangeListener(this);
plcAddresses.values().forEach(plcAddress -> plc.register(plcAddress, this::queueUpdateState));
if (tx.getConfiguration().hasParameter(PARAM_VERBOSE)) {
boolean verboseOverride = tx.getConfiguration().getBoolean(PARAM_VERBOSE);
logger.info("Overriding XML verbose property from configuration resource to " + verboseOverride);
this.verbose = verboseOverride;
}
if (tx.needsCommit())
tx.commitOnClose();
}
return plc;
}
private PrivilegeContext validateCtx() {
if (this.ctx == null) {
this.ctx = getContainer().getPrivilegeHandler().openAgentSystemUserContext();
} else {
try {
getContainer().getPrivilegeHandler().validateSystemSession(this.ctx);
} catch (Exception e) {
logger.error("PrivilegeContext for session " + this.ctx.getCertificate().getSessionId()
+ " is not valid, reopening.", e);
this.ctx = getContainer().getPrivilegeHandler().openAgentSystemUserContext();
}
}
return this.ctx;
}
@Override
public void setGlobalListener(GlobalPlcListener listener) {
this.globalListener = listener;
if (this.plc != null)
this.plc.setGlobalListener(listener);
}
@Override
public void register(String resource, String action, PlcListener listener) {
PlcAddress plcAddress = this.plcAddresses.getElement(resource, action);
if (plcAddress == null)
throw new IllegalStateException("No PlcAddress exists for " + resource + "-" + action);
this.plc.register(plcAddress, listener);
}
@Override
public void unregister(String resource, String action, PlcListener listener) {
PlcAddress plcAddress = this.plcAddresses.getElement(resource, action);
if (plcAddress == null) {
logger.warn("No PlcAddress exists for " + resource + "-" + action);
} else {
this.plc.unregister(plcAddress, listener);
}
}
private void queueUpdateState(PlcAddress plcAddress, Object o) {
this.updateStateQueue.add(() -> updatePlcAddress(plcAddress, o));
}
private void queueUpdateState(PlcConnection connection) {
this.updateStateQueue.add(
() -> updateConnectionState(connection.getId(), connection.getState(), connection.getStateMsg()));
}
@Override
public void sendMsg(LogMessage message) {
addMsg(listener -> listener.sendMsg(message));
}
@Override
public void disableMsg(Locator locator) {
addMsg(listener -> listener.disableMsg(locator));
}
private synchronized void addMsg(Consumer<GlobalPlcListener> consumer) {
if (this.messageQueue.size() > MAX_MESSAGE_QUEUE)
this.messageQueue.removeFirst();
this.messageQueue.addLast(consumer);
}
private void sendMessages() {
while (this.run) {
try {
while (this.globalListener == null) {
Thread.sleep(100L);
}
this.messageQueue.takeFirst().accept(this.globalListener);
} catch (InterruptedException e) {
logger.warn("Interrupted");
} catch (Exception e) {
logger.error("Failed to send message", e);
}
}
}
private void updateStates() {
logger.info("Update State Handler running...");
while (this.run) {
try {
this.updateStateQueue.take().run();
} catch (InterruptedException e) {
logger.error("Interrupted!");
} catch (Exception e) {
logger.error("Failed to perform state update", e);
}
}
logger.info("Update State Handler stopped.");
}
private void updatePlcAddress(PlcAddress address, Object value) {
long s = 0L;
if (this.verbose)
s = nanoTime();
String addressId = this.addressesToResourceId.get(address);
if (addressId == null) {
logger.error("No PlcAddress mapping for " + address);
return;
}
Certificate cert = validateCtx().getCertificate();
try (StrolchTransaction tx = openTx(cert, getCallerMethod(), false).silentThreshold(SILENT_THRESHOLD,
MILLISECONDS)) {
tx.lock(Resource.locatorFor(TYPE_PLC_ADDRESS, addressId));
Resource addressRes = tx.getResourceBy(TYPE_PLC_ADDRESS, addressId, true);
Parameter<?> valueP = addressRes.getParameter(PARAM_VALUE, true);
if (valueP.getValue().equals(value)) {
if (this.verbose)
logger.info("Ignoring PlcAddress {} unchanged value {}", address.toKey(), value);
return;
}
if (this.verbose)
logger.info("PlcAddress {} has changed from {} to {}", address.toKey(), valueP.getValue(), value);
valueP.accept(new SetParameterValueVisitor(value));
tx.update(addressRes);
tx.commitOnClose();
} catch (Exception e) {
logger.error("Failed to update PlcAddress " + addressId + " with new value " + value, e);
}
if (this.verbose)
logger.info("async update " + address.toKey() + " took " + (formatNanoDuration(nanoTime() - s)));
}
private void updateConnectionState(String id, ConnectionState state, String stateMsg) {
long s = 0L;
if (this.verbose)
s = nanoTime();
try (StrolchTransaction tx = openTx(validateCtx().getCertificate(), getCallerMethod(), false).silentThreshold(
SILENT_THRESHOLD, MILLISECONDS)) {
tx.lock(Resource.locatorFor(TYPE_PLC_CONNECTION, id));
Resource connection = tx.getResourceBy(TYPE_PLC_CONNECTION, id);
StringParameter stateP = connection.getParameter(BAG_PARAMETERS, PARAM_STATE, true);
StringParameter stateMsgP = connection.getParameter(BAG_PARAMETERS, PARAM_STATE_MSG, true);
logger.info("State for PlcConnection {} has changed from {} to {}", connection.getId(), stateP.getValue(),
state.name());
stateP.setValue(state.name());
stateMsgP.setValue(stateMsg);
tx.update(connection);
tx.commitOnClose();
} catch (Exception e) {
logger.error("Failed to update state for connection " + id, e);
}
if (this.verbose)
logger.info("updateConnectionState took " + (formatNanoDuration(nanoTime() - s)));
}
@Override
public void send(String resource, String action) {
send(resource, action, true, true);
}
@Override
public void send(String resource, String action, Object value) {
send(resource, action, value, true, true);
}
@Override
public void send(String resource, String action, boolean catchExceptions, boolean notifyGlobalListener) {
PlcAddress plcAddress = this.plcTelegrams.getElement(resource, action);
if (plcAddress == null)
throw new IllegalStateException("No PlcTelegram exists for " + resource + "-" + action);
if (plcAddress.defaultValue == null)
throw new IllegalStateException("Can not send PlcAddress as no default value set for " + plcAddress);
this.plc.send(plcAddress, catchExceptions, notifyGlobalListener);
}
@Override
public void send(String resource, String action, Object value, boolean catchExceptions,
boolean notifyGlobalListener) {
PlcAddress plcAddress = this.plcTelegrams.getElement(resource, action);
if (plcAddress == null)
throw new IllegalStateException("No PlcTelegram exists for " + resource + "-" + action);
this.plc.send(plcAddress, value, catchExceptions, notifyGlobalListener);
}
@Override
public void notify(String resource, String action, Object value) {
PlcAddress plcAddress = this.plcAddresses.getElement(resource, action);
if (plcAddress == null)
throw new IllegalStateException("No PlcAddress exists for " + resource + "-" + action);
if (plcAddress.type != PlcAddressType.Notification)
throw new IllegalStateException(
"Can not notify PlcAddress " + plcAddress + " as it is not a notification!");
this.plc.syncNotify(plcAddress.address, value);
}
@Override
public StrolchTransaction openTx(Certificate cert, boolean readOnly) {
return super.openTx(cert, readOnly);
}
@Override
public void notifyStateChange(PlcConnection connection) {
queueUpdateState(connection);
}
}