Initial commit
Some checks failed
Gradle Build / build (push) Has been cancelled

This commit is contained in:
CaiXiang
2024-11-30 18:36:13 +08:00
commit aa56926258
2134 changed files with 232943 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: The openTCS Authors
# SPDX-License-Identifier: CC-BY-4.0

View File

@@ -0,0 +1,70 @@
# SPDX-FileCopyrightText: The openTCS Authors
# SPDX-License-Identifier: CC-BY-4.0
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
#handlers= java.util.logging.ConsoleHandler
# To also add the FileHandler, use the following line instead.
handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= INFO
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = ./log/opentcs-kernel.%g.log
java.util.logging.FileHandler.limit = 500000
java.util.logging.FileHandler.count = 10
#java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.FileHandler.formatter = org.opentcs.util.logging.SingleLineFormatter
java.util.logging.FileHandler.append = true
java.util.logging.FileHandler.level = FINE
# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = FINE
#java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.ConsoleHandler.formatter = org.opentcs.util.logging.SingleLineFormatter
# Our own handler for the GUI:
#org.opentcs.kernel.controlcenter.ControlCenterInfoHandler.level = WARNING
############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################
# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
#com.xyz.foo.level = SEVERE
# Logging configuration for single classes. Remember that you might also have to
# adjust handler levels!
#org.opentcs.strategies.basic.dispatching.DefaultDispatcher.level = FINE
#org.opentcs.kernel.KernelStateOperating.level = FINE

View File

@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: The openTCS Authors
# SPDX-License-Identifier: CC-BY-4.0

View File

@@ -0,0 +1,62 @@
@echo off
rem SPDX-FileCopyrightText: The openTCS Authors
rem SPDX-License-Identifier: MIT
rem
rem Start SSL keystore and truststore generation.
rem
rem Don't export variables to the parent shell.
setlocal
rem Set the path to where keytool is located. Keytool comes with the Java JRE.
set KEYTOOL_PATH="keytool"
rem Try to execute keytool
%KEYTOOL_PATH% 2>nul
if %ERRORLEVEL% neq 0 (
if %KEYTOOL_PATH% equ "keytool" (
echo Error: Could not find keytool in PATH.
) else (
echo Error: Could not find keytool in %KEYTOOL_PATH%.
)
exit /B %ERRORLEVEL%
)
rem Set base directory names.
set OPENTCS_HOME=.
set OPENTCS_CONFIGDIR=%OPENTCS_HOME%\config
set OUTPUTDIR=%OPENTCS_CONFIGDIR%
rem Set paths to generate files at.
set KEYSTORE_FILEPATH=%OUTPUTDIR%\keystore.p12
set TRUSTSTORE_FILEPATH=%OUTPUTDIR%\truststore.p12
set CERTIFICATE_FILEPATH=%OUTPUTDIR%\certificate.cer
rem Set the password used for generating the stores.
set PASSWORD=password
echo Deleting previously generated keystore and truststore...
del %KEYSTORE_FILEPATH% 2>nul
del %TRUSTSTORE_FILEPATH% 2>nul
del %CERTIFICATE_FILEPATH% 2>nul
rem Generates a keypair wrapped in a self-signed (X.509) certificate.
rem Some defaults of the -genkeypair command: -alias "mykey" -keyalg "DSA" -keysize 1024 -validity 90
echo Generating a new keystore in %KEYSTORE_FILEPATH%...
%KEYTOOL_PATH% -genkeypair -alias openTCS -keyalg RSA -dname "c=DE" -storepass %PASSWORD% -keypass %PASSWORD% -validity 365 -storetype PKCS12 -keystore %KEYSTORE_FILEPATH%
rem Exports the (wrapping) self-signed certificate from the generated keypair.
rem '-rfc' - Output the certificate in a printable encoding format (by default the -export command outputs a certificate in binary encoding)
rem '2>nul' - Suppress output of this command
%KEYTOOL_PATH% -exportcert -alias openTCS -file %CERTIFICATE_FILEPATH% -keystore %KEYSTORE_FILEPATH% -storepass %PASSWORD% -rfc 2>nul
rem Adds the exported certificate to a new keystore and its trusted certificates.
echo Generating a new truststore in %TRUSTSTORE_FILEPATH%...
%KEYTOOL_PATH% -importcert -alias openTCS -file %CERTIFICATE_FILEPATH% -storepass %PASSWORD% -storetype PKCS12 -keystore %TRUSTSTORE_FILEPATH% -noprompt 2>nul
rem Delete the exported certificate since it's not really needed.
del %CERTIFICATE_FILEPATH%
echo Copy the generated truststore to the openTCS PlantOverview's \config folder or a corresponding location of your application.
pause

View File

@@ -0,0 +1,57 @@
#!/bin/sh
# SPDX-FileCopyrightText: The openTCS Authors
# SPDX-License-Identifier: MIT
#
# Start SSL keystore and truststore generation.
#
# Set the path to where keytool is located. Keytool comes with the Java JRE.
export KEYTOOL_PATH="keytool"
# Try to execute keytool
${KEYTOOL_PATH} 2>/dev/null
if [ $? -ne 0 ]; then
if [ "${KEYTOOL_PATH}" = "keytool" ]; then
echo Error: Could not find keytool in PATH.
else
echo Error: Could not find keytool in ${KEYTOOL_PATH}.
fi
exit $?
fi
# Set base directory names.
export OPENTCS_HOME=.
export OPENTCS_CONFIGDIR="${OPENTCS_HOME}/config"
export OUTPUTDIR="${OPENTCS_CONFIGDIR}"
# Set paths to generate files at.
export KEYSTORE_FILEPATH="${OUTPUTDIR}/keystore.p12"
export TRUSTSTORE_FILEPATH="${OUTPUTDIR}/truststore.p12"
export CERTIFICATE_FILEPATH="${OUTPUTDIR}/certificate.cer"
# Set the password used for generating the stores.
export PASSWORD=password
echo Deleting previously generated keystore and truststore...
rm ${KEYSTORE_FILEPATH} 2>/dev/null
rm ${TRUSTSTORE_FILEPATH} 2>/dev/null
rm ${CERTIFICATE_FILEPATH} 2>/dev/null
# Generates a keypair wrapped in a self-signed (X.509) certificate.
# Some defaults of the -genkeypair command: -alias "mykey" -keyalg "DSA" -keysize 1024 -validity 90
echo Generating a new keystore in ${KEYSTORE_FILEPATH}...
${KEYTOOL_PATH} -genkeypair -alias openTCS -keyalg RSA -dname "c=DE" -storepass ${PASSWORD} -keypass ${PASSWORD} -validity 365 -storetype PKCS12 -keystore ${KEYSTORE_FILEPATH}
# Exports the (wrapping) self-signed certificate from the generated keypair.
# '-rfc' - Output the certificate in a printable encoding format (by default the -export command outputs a certificate in binary encoding)
# '2>nul' - Suppress output of this command
${KEYTOOL_PATH} -exportcert -alias openTCS -file ${CERTIFICATE_FILEPATH} -keystore ${KEYSTORE_FILEPATH} -storepass ${PASSWORD} -rfc 2>/dev/null
# Adds the exported certificate to a new keystore and its trusted certificates.
echo Generating a new truststore in ${TRUSTSTORE_FILEPATH}...
${KEYTOOL_PATH} -importcert -alias openTCS -file ${CERTIFICATE_FILEPATH} -storepass ${PASSWORD} -storetype PKCS12 -keystore ${TRUSTSTORE_FILEPATH} -noprompt 2>/dev/null
# Delete the exported certificate since it's not really needed.
rm ${CERTIFICATE_FILEPATH}
echo Copy the generated truststore to the openTCS PlantOverview\'s /config folder or a corresponding location of your application.

View File

View File

View File

@@ -0,0 +1,20 @@
@echo off
rem SPDX-FileCopyrightText: The openTCS Authors
rem SPDX-License-Identifier: MIT
rem
rem Shut down the openTCS kernel.
rem
rem Don't export variables to the parent shell
setlocal
rem Set base directory names.
set OPENTCS_BASE=.
set OPENTCS_LIBDIR=%OPENTCS_BASE%\lib
rem Set the class path
set OPENTCS_CP=%OPENTCS_LIBDIR%\*;
set OPENTCS_CP=%OPENTCS_CP%;%OPENTCS_LIBDIR%\openTCS-extensions\*;
java -classpath "%OPENTCS_CP%" ^
org.opentcs.kernel.ShutdownKernel localhost 55100

View File

@@ -0,0 +1,24 @@
#!/bin/sh
# SPDX-FileCopyrightText: The openTCS Authors
# SPDX-License-Identifier: MIT
#
# Shut down the openTCS kernel.
#
# Set base directory names.
export OPENTCS_BASE=.
export OPENTCS_LIBDIR="${OPENTCS_BASE}/lib"
# Set the class path
export OPENTCS_CP="${OPENTCS_LIBDIR}/*"
export OPENTCS_CP="${OPENTCS_CP}:${OPENTCS_LIBDIR}/openTCS-extensions/*"
if [ -n "${OPENTCS_JAVAVM}" ]; then
export JAVA="${OPENTCS_JAVAVM}"
else
# XXX Be a bit more clever to find out the name of the JVM runtime.
export JAVA="java"
fi
${JAVA} -classpath "${OPENTCS_CP}" \
org.opentcs.kernel.ShutdownKernel localhost 55100

36
opentcs-kernel/src/dist/startKernel.bat vendored Normal file
View File

@@ -0,0 +1,36 @@
@echo off
rem SPDX-FileCopyrightText: The openTCS Authors
rem SPDX-License-Identifier: MIT
rem
rem Start the openTCS kernel.
rem
rem Set window title
title Kernel (openTCS)
rem Don't export variables to the parent shell
setlocal
rem Set base directory names.
set OPENTCS_BASE=.
set OPENTCS_HOME=.
set OPENTCS_CONFIGDIR=%OPENTCS_HOME%\config
set OPENTCS_LIBDIR=%OPENTCS_BASE%\lib
rem Set the class path
set OPENTCS_CP=%OPENTCS_LIBDIR%\*;
set OPENTCS_CP=%OPENTCS_CP%;%OPENTCS_LIBDIR%\openTCS-extensions\*;
rem XXX Be a bit more clever to find out the name of the JVM runtime.
set JAVA=java
rem Start kernel
%JAVA% -enableassertions ^
-Dopentcs.base="%OPENTCS_BASE%" ^
-Dopentcs.home="%OPENTCS_HOME%" ^
-Dopentcs.configuration.provider=gestalt ^
-Dopentcs.configuration.reload.interval=10000 ^
-Djava.util.logging.config.file="%OPENTCS_CONFIGDIR%\logging.config" ^
-XX:-OmitStackTraceInFastThrow ^
-classpath "%OPENTCS_CP%" ^
org.opentcs.kernel.RunKernel

34
opentcs-kernel/src/dist/startKernel.sh vendored Normal file
View File

@@ -0,0 +1,34 @@
#!/bin/sh
# SPDX-FileCopyrightText: The openTCS Authors
# SPDX-License-Identifier: MIT
#
# Start the openTCS kernel.
#
# Set base directory names.
export OPENTCS_BASE=.
export OPENTCS_HOME=.
export OPENTCS_CONFIGDIR="${OPENTCS_HOME}/config"
export OPENTCS_LIBDIR="${OPENTCS_BASE}/lib"
# Set the class path
export OPENTCS_CP="${OPENTCS_LIBDIR}/*"
export OPENTCS_CP="${OPENTCS_CP}:${OPENTCS_LIBDIR}/openTCS-extensions/*"
if [ -n "${OPENTCS_JAVAVM}" ]; then
export JAVA="${OPENTCS_JAVAVM}"
else
# XXX Be a bit more clever to find out the name of the JVM runtime.
export JAVA="java"
fi
# Start kernel
${JAVA} -enableassertions \
-Dopentcs.base="${OPENTCS_BASE}" \
-Dopentcs.home="${OPENTCS_HOME}" \
-Dopentcs.configuration.provider=gestalt \
-Dopentcs.configuration.reload.interval=10000 \
-Djava.util.logging.config.file=${OPENTCS_CONFIGDIR}/logging.config \
-XX:-OmitStackTraceInFastThrow \
-classpath "${OPENTCS_CP}" \
org.opentcs.kernel.RunKernel

View File

@@ -0,0 +1,338 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import com.google.inject.multibindings.MapBinder;
import jakarta.inject.Singleton;
import java.io.File;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import org.opentcs.access.Kernel;
import org.opentcs.access.LocalKernel;
import org.opentcs.access.SslParameterSet;
import org.opentcs.common.LoggingScheduledThreadPoolExecutor;
import org.opentcs.components.kernel.ObjectNameProvider;
import org.opentcs.components.kernel.services.DispatcherService;
import org.opentcs.components.kernel.services.InternalPeripheralJobService;
import org.opentcs.components.kernel.services.InternalPeripheralService;
import org.opentcs.components.kernel.services.InternalPlantModelService;
import org.opentcs.components.kernel.services.InternalQueryService;
import org.opentcs.components.kernel.services.InternalTransportOrderService;
import org.opentcs.components.kernel.services.InternalVehicleService;
import org.opentcs.components.kernel.services.NotificationService;
import org.opentcs.components.kernel.services.PeripheralDispatcherService;
import org.opentcs.components.kernel.services.PeripheralJobService;
import org.opentcs.components.kernel.services.PeripheralService;
import org.opentcs.components.kernel.services.PlantModelService;
import org.opentcs.components.kernel.services.QueryService;
import org.opentcs.components.kernel.services.RouterService;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.components.kernel.services.TransportOrderService;
import org.opentcs.components.kernel.services.VehicleService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.customizations.ApplicationHome;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.customizations.kernel.KernelExecutor;
import org.opentcs.customizations.kernel.KernelInjectionModule;
import org.opentcs.drivers.peripherals.PeripheralControllerPool;
import org.opentcs.drivers.vehicle.VehicleControllerPool;
import org.opentcs.kernel.extensions.controlcenter.vehicles.AttachmentManager;
import org.opentcs.kernel.extensions.controlcenter.vehicles.VehicleEntryPool;
import org.opentcs.kernel.extensions.watchdog.Watchdog;
import org.opentcs.kernel.extensions.watchdog.WatchdogConfiguration;
import org.opentcs.kernel.peripherals.DefaultPeripheralControllerPool;
import org.opentcs.kernel.peripherals.LocalPeripheralControllerPool;
import org.opentcs.kernel.peripherals.PeripheralAttachmentManager;
import org.opentcs.kernel.peripherals.PeripheralCommAdapterRegistry;
import org.opentcs.kernel.peripherals.PeripheralControllerFactory;
import org.opentcs.kernel.peripherals.PeripheralEntryPool;
import org.opentcs.kernel.persistence.ModelPersister;
import org.opentcs.kernel.persistence.XMLFileModelPersister;
import org.opentcs.kernel.services.StandardDispatcherService;
import org.opentcs.kernel.services.StandardNotificationService;
import org.opentcs.kernel.services.StandardPeripheralDispatcherService;
import org.opentcs.kernel.services.StandardPeripheralJobService;
import org.opentcs.kernel.services.StandardPeripheralService;
import org.opentcs.kernel.services.StandardPlantModelService;
import org.opentcs.kernel.services.StandardQueryService;
import org.opentcs.kernel.services.StandardRouterService;
import org.opentcs.kernel.services.StandardTCSObjectService;
import org.opentcs.kernel.services.StandardTransportOrderService;
import org.opentcs.kernel.services.StandardVehicleService;
import org.opentcs.kernel.vehicles.DefaultVehicleControllerPool;
import org.opentcs.kernel.vehicles.LocalVehicleControllerPool;
import org.opentcs.kernel.vehicles.VehicleCommAdapterRegistry;
import org.opentcs.kernel.vehicles.VehicleControllerComponentsFactory;
import org.opentcs.kernel.vehicles.VehicleControllerFactory;
import org.opentcs.kernel.vehicles.transformers.CoordinateSystemTransformerFactory;
import org.opentcs.kernel.vehicles.transformers.DefaultVehicleDataTransformerFactory;
import org.opentcs.kernel.workingset.CreationTimeThreshold;
import org.opentcs.kernel.workingset.NotificationBuffer;
import org.opentcs.kernel.workingset.PeripheralJobPoolManager;
import org.opentcs.kernel.workingset.PlantModelManager;
import org.opentcs.kernel.workingset.PrefixedUlidObjectNameProvider;
import org.opentcs.kernel.workingset.TCSObjectManager;
import org.opentcs.kernel.workingset.TCSObjectRepository;
import org.opentcs.kernel.workingset.TransportOrderPoolManager;
import org.opentcs.util.event.EventBus;
import org.opentcs.util.event.EventHandler;
import org.opentcs.util.event.SimpleEventBus;
import org.opentcs.util.logging.UncaughtExceptionLogger;
/**
* A Guice module for the openTCS kernel application.
*/
public class DefaultKernelInjectionModule
extends
KernelInjectionModule {
/**
* Creates a new instance.
*/
public DefaultKernelInjectionModule() {
}
@Override
protected void configure() {
configureEventHub();
configureKernelExecutor();
// Ensure that the application's home directory can be used everywhere.
File applicationHome = new File(System.getProperty("opentcs.home", "."));
bind(File.class)
.annotatedWith(ApplicationHome.class)
.toInstance(applicationHome);
// A single global synchronization object for the kernel.
bind(Object.class)
.annotatedWith(GlobalSyncObject.class)
.to(Object.class)
.in(Singleton.class);
// The kernel's data pool structures.
bind(TCSObjectRepository.class).in(Singleton.class);
bind(TCSObjectManager.class).in(Singleton.class);
bind(PlantModelManager.class).in(Singleton.class);
bind(TransportOrderPoolManager.class).in(Singleton.class);
bind(PeripheralJobPoolManager.class).in(Singleton.class);
bind(NotificationBuffer.class).in(Singleton.class);
bind(ObjectNameProvider.class)
.to(PrefixedUlidObjectNameProvider.class)
.in(Singleton.class);
configurePersistence();
bind(VehicleCommAdapterRegistry.class)
.in(Singleton.class);
configureVehicleControllers();
bind(AttachmentManager.class)
.in(Singleton.class);
bind(VehicleEntryPool.class)
.in(Singleton.class);
configurePeripheralControllers();
bind(PeripheralCommAdapterRegistry.class)
.in(Singleton.class);
bind(PeripheralAttachmentManager.class)
.in(Singleton.class);
bind(PeripheralEntryPool.class)
.in(Singleton.class);
bind(StandardKernel.class)
.in(Singleton.class);
bind(LocalKernel.class)
.to(StandardKernel.class);
vehicleDataTransformersBinder().addBinding().to(DefaultVehicleDataTransformerFactory.class);
// tag::documentation_registerTransformerFactory[]
vehicleDataTransformersBinder().addBinding().to(CoordinateSystemTransformerFactory.class);
// end::documentation_registerTransformerFactory[]
configureKernelStatesDependencies();
configureKernelStarterDependencies();
configureSslParameters();
configureKernelServicesDependencies();
// Ensure all of these binders are initialized.
extensionsBinderAllModes();
extensionsBinderModelling();
extensionsBinderOperating();
vehicleCommAdaptersBinder();
peripheralCommAdaptersBinder();
configureWatchdogExtension();
}
private void configureKernelServicesDependencies() {
bind(StandardPlantModelService.class).in(Singleton.class);
bind(PlantModelService.class).to(StandardPlantModelService.class);
bind(InternalPlantModelService.class).to(StandardPlantModelService.class);
bind(StandardTransportOrderService.class).in(Singleton.class);
bind(TransportOrderService.class).to(StandardTransportOrderService.class);
bind(InternalTransportOrderService.class).to(StandardTransportOrderService.class);
bind(StandardVehicleService.class).in(Singleton.class);
bind(VehicleService.class).to(StandardVehicleService.class);
bind(InternalVehicleService.class).to(StandardVehicleService.class);
bind(StandardTCSObjectService.class).in(Singleton.class);
bind(TCSObjectService.class).to(StandardTCSObjectService.class);
bind(StandardNotificationService.class).in(Singleton.class);
bind(NotificationService.class).to(StandardNotificationService.class);
bind(StandardRouterService.class).in(Singleton.class);
bind(RouterService.class).to(StandardRouterService.class);
bind(StandardDispatcherService.class).in(Singleton.class);
bind(DispatcherService.class).to(StandardDispatcherService.class);
bind(StandardQueryService.class).in(Singleton.class);
bind(QueryService.class).to(StandardQueryService.class);
bind(InternalQueryService.class).to(StandardQueryService.class);
bind(StandardPeripheralService.class).in(Singleton.class);
bind(PeripheralService.class).to(StandardPeripheralService.class);
bind(InternalPeripheralService.class).to(StandardPeripheralService.class);
bind(StandardPeripheralJobService.class).in(Singleton.class);
bind(PeripheralJobService.class).to(StandardPeripheralJobService.class);
bind(InternalPeripheralJobService.class).to(StandardPeripheralJobService.class);
bind(StandardPeripheralDispatcherService.class).in(Singleton.class);
bind(PeripheralDispatcherService.class).to(StandardPeripheralDispatcherService.class);
}
private void configureVehicleControllers() {
install(new FactoryModuleBuilder().build(VehicleControllerFactory.class));
install(new FactoryModuleBuilder().build(VehicleControllerComponentsFactory.class));
bind(DefaultVehicleControllerPool.class)
.in(Singleton.class);
bind(VehicleControllerPool.class)
.to(DefaultVehicleControllerPool.class);
bind(LocalVehicleControllerPool.class)
.to(DefaultVehicleControllerPool.class);
}
private void configurePeripheralControllers() {
install(new FactoryModuleBuilder().build(PeripheralControllerFactory.class));
bind(DefaultPeripheralControllerPool.class)
.in(Singleton.class);
bind(PeripheralControllerPool.class)
.to(DefaultPeripheralControllerPool.class);
bind(LocalPeripheralControllerPool.class)
.to(DefaultPeripheralControllerPool.class);
}
private void configurePersistence() {
bind(ModelPersister.class).to(XMLFileModelPersister.class);
}
private void configureEventHub() {
EventBus newEventBus = new SimpleEventBus();
bind(EventHandler.class)
.annotatedWith(ApplicationEventBus.class)
.toInstance(newEventBus);
bind(org.opentcs.util.event.EventSource.class)
.annotatedWith(ApplicationEventBus.class)
.toInstance(newEventBus);
bind(EventBus.class)
.annotatedWith(ApplicationEventBus.class)
.toInstance(newEventBus);
}
private void configureKernelStatesDependencies() {
// A map for KernelState instances to be provided at runtime.
MapBinder<Kernel.State, KernelState> stateMapBinder
= MapBinder.newMapBinder(binder(), Kernel.State.class, KernelState.class);
stateMapBinder.addBinding(Kernel.State.SHUTDOWN).to(KernelStateShutdown.class);
stateMapBinder.addBinding(Kernel.State.MODELLING).to(KernelStateModelling.class);
stateMapBinder.addBinding(Kernel.State.OPERATING).to(KernelStateOperating.class);
bind(OrderPoolConfiguration.class)
.toInstance(
getConfigBindingProvider().get(
OrderPoolConfiguration.PREFIX,
OrderPoolConfiguration.class
)
);
bind(CreationTimeThreshold.class)
.in(Singleton.class);
transportOrderCleanupApprovalBinder();
orderSequenceCleanupApprovalBinder();
peripheralJobCleanupApprovalBinder();
}
private void configureKernelStarterDependencies() {
bind(KernelApplicationConfiguration.class)
.toInstance(
getConfigBindingProvider().get(
KernelApplicationConfiguration.PREFIX,
KernelApplicationConfiguration.class
)
);
}
private void configureSslParameters() {
SslConfiguration configuration
= getConfigBindingProvider().get(
SslConfiguration.PREFIX,
SslConfiguration.class
);
SslParameterSet sslParamSet = new SslParameterSet(
SslParameterSet.DEFAULT_KEYSTORE_TYPE,
new File(configuration.keystoreFile()),
configuration.keystorePassword(),
new File(configuration.truststoreFile()),
configuration.truststorePassword()
);
bind(SslParameterSet.class).toInstance(sslParamSet);
}
private void configureKernelExecutor() {
ScheduledExecutorService executor
= new LoggingScheduledThreadPoolExecutor(
1,
runnable -> {
Thread thread = new Thread(runnable, "kernelExecutor");
thread.setUncaughtExceptionHandler(new UncaughtExceptionLogger(false));
return thread;
}
);
bind(ScheduledExecutorService.class)
.annotatedWith(KernelExecutor.class)
.toInstance(executor);
bind(ExecutorService.class)
.annotatedWith(KernelExecutor.class)
.toInstance(executor);
bind(Executor.class)
.annotatedWith(KernelExecutor.class)
.toInstance(executor);
}
private void configureWatchdogExtension() {
extensionsBinderOperating().addBinding()
.to(Watchdog.class)
.in(Singleton.class);
bind(WatchdogConfiguration.class)
.toInstance(
getConfigBindingProvider().get(
WatchdogConfiguration.PREFIX,
WatchdogConfiguration.class
)
);
}
}

View File

@@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.util.Modules;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ServiceLoader;
import org.opentcs.configuration.ConfigurationBindingProvider;
import org.opentcs.configuration.gestalt.GestaltConfigurationBindingProvider;
import org.opentcs.customizations.kernel.KernelInjectionModule;
import org.opentcs.strategies.basic.dispatching.DefaultDispatcherModule;
import org.opentcs.strategies.basic.peripherals.dispatching.DefaultPeripheralJobDispatcherModule;
import org.opentcs.strategies.basic.routing.DefaultRouterModule;
import org.opentcs.strategies.basic.scheduling.DefaultSchedulerModule;
import org.opentcs.util.Environment;
import org.opentcs.util.logging.UncaughtExceptionLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The kernel process's default entry point.
*/
public class RunKernel {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(RunKernel.class);
/**
* Prevents external instantiation.
*/
private RunKernel() {
}
/**
* Initializes the system and starts the openTCS kernel including modules.
*
* @param args The command line arguments.
* @throws Exception If there was a problem starting the kernel.
*/
public static void main(String[] args)
throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(false));
Environment.logSystemInfo();
LOG.debug("Setting up openTCS kernel {}...", Environment.getBaselineVersion());
Injector injector = Guice.createInjector(customConfigurationModule());
injector.getInstance(KernelStarter.class).startKernel();
}
/**
* Builds and returns a Guice module containing the custom configuration for the kernel
* application, including additions and overrides by the user.
*
* @return The custom configuration module.
*/
private static Module customConfigurationModule() {
List<KernelInjectionModule> defaultModules
= Arrays.asList(
new DefaultKernelInjectionModule(),
new DefaultDispatcherModule(),
new DefaultRouterModule(),
new DefaultSchedulerModule(),
new DefaultPeripheralJobDispatcherModule()
);
ConfigurationBindingProvider bindingProvider = configurationBindingProvider();
for (KernelInjectionModule defaultModule : defaultModules) {
defaultModule.setConfigBindingProvider(bindingProvider);
}
return Modules.override(defaultModules)
.with(findRegisteredModules(bindingProvider));
}
/**
* Finds and returns all Guice modules registered via ServiceLoader.
*
* @return The registered/found modules.
*/
private static List<KernelInjectionModule> findRegisteredModules(
ConfigurationBindingProvider bindingProvider
) {
List<KernelInjectionModule> registeredModules = new ArrayList<>();
for (KernelInjectionModule module : ServiceLoader.load(KernelInjectionModule.class)) {
LOG.info(
"Integrating injection module {} (source: {})",
module.getClass().getName(),
module.getClass().getProtectionDomain().getCodeSource()
);
module.setConfigBindingProvider(bindingProvider);
registeredModules.add(module);
}
return registeredModules;
}
private static ConfigurationBindingProvider configurationBindingProvider() {
String chosenProvider = System.getProperty("opentcs.configuration.provider", "gestalt");
switch (chosenProvider) {
case "gestalt":
default:
LOG.info("Using gestalt as the configuration provider.");
return gestaltConfigurationBindingProvider();
}
}
private static ConfigurationBindingProvider gestaltConfigurationBindingProvider() {
return new GestaltConfigurationBindingProvider(
Paths.get(
System.getProperty("opentcs.base", "."),
"config",
"opentcs-kernel-defaults-baseline.properties"
)
.toAbsolutePath(),
Paths.get(
System.getProperty("opentcs.base", "."),
"config",
"opentcs-kernel-defaults-custom.properties"
)
.toAbsolutePath(),
Paths.get(
System.getProperty("opentcs.home", "."),
"config",
"opentcs-kernel.properties"
)
.toAbsolutePath()
);
}
}

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
/**
* Shuts down a running kernel via its administration interface.
*/
public class ShutdownKernel {
private ShutdownKernel() {
}
/**
* Java main.
*
* @param args command line args
*/
public static void main(String[] args) {
if (args.length > 2) {
System.err.println("ShutdownKernel [<host>] [<port>]");
return;
}
String hostName = args.length > 0 ? args[0] : "localhost";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 55001;
try {
URL url = new URI("http://" + hostName + ":" + port + "/v1/kernel").toURL();
System.err.println("Calling to " + url + "...");
HttpURLConnection httpCon = (HttpURLConnection) url.openConnection();
httpCon.setRequestMethod("DELETE");
httpCon.connect();
httpCon.getInputStream();
}
catch (IOException | URISyntaxException exc) {
System.err.println("Exception accessing admin interface:");
exc.printStackTrace();
}
}
}

View File

@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: The openTCS Authors
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import org.opentcs.configuration.ConfigurationEntry;
import org.opentcs.configuration.ConfigurationPrefix;
/**
* Provides common kernel configuration entries.
*/
@ConfigurationPrefix(KernelApplicationConfiguration.PREFIX)
public interface KernelApplicationConfiguration {
/**
* This configuration's prefix.
*/
String PREFIX = "kernelapp";
@ConfigurationEntry(
type = "Boolean",
description = "Whether to automatically enable drivers on startup.",
changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL,
orderKey = "1_startup_0"
)
boolean autoEnableDriversOnStartup();
@ConfigurationEntry(
type = "Boolean",
description = "Whether to automatically enable peripheral drivers on startup.",
changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL,
orderKey = "1_startup_1"
)
boolean autoEnablePeripheralDriversOnStartup();
@ConfigurationEntry(
type = "Boolean",
description = "Whether to implicitly save the model when leaving modelling state.",
changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL,
orderKey = "2_autosave"
)
boolean saveModelOnTerminateModelling();
@ConfigurationEntry(
type = "Boolean",
description = "Whether to implicitly save the model when leaving operating state.",
changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL,
orderKey = "2_autosave"
)
boolean saveModelOnTerminateOperating();
@ConfigurationEntry(
type = "Boolean",
description = "Whether to implicitly update the router's topology when a path is (un)locked.",
changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY,
orderKey = "3_topologyUpdate"
)
boolean updateRoutingTopologyOnPathLockChange();
@ConfigurationEntry(
type = "Boolean",
description = "Whether vehicles should be rerouted immediately on topology changes.",
changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY,
orderKey = "4_reroute_1"
)
boolean rerouteOnRoutingTopologyUpdate();
@ConfigurationEntry(
type = "Boolean",
description = "Whether vehicles should be rerouted as soon as they finish a drive order.",
changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY,
orderKey = "4_reroute_2"
)
boolean rerouteOnDriveOrderFinished();
@ConfigurationEntry(
type = "String",
description = {
"The type of how vehicle resources (i.e., paths, points and locations allocated by "
+ "vehicles) are managed.",
"Possible values:",
"LENGTH_IGNORED: Resources are _always_ released up to (excluding) a vehicle's current"
+ "position. This type can be useful when you primarily want to utilize vehicle "
+ "envelopes for traffic management.",
"""
LENGTH_RESPECTED: Only resources that are no longer "covered" by a vehicle (according
to the length of the vehicle and the length of the paths behind it) are released. This
is the "classic" way resources were managed before vehicle envelopes were introduced.
"""
},
changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY,
orderKey = "5_resource_management_1"
)
VehicleResourceManagementType vehicleResourceManagementType();
/**
* Defines the different types of how vehicle resources (i.e., paths, points and locations
* allocated by vehicles) are managed.
*/
enum VehicleResourceManagementType {
/**
* When releasing resources, the length of a vehicle is ignored.
* <p>
* Resources are <em>always</em> released up to (excluding) a vehicle's current position.
* </p>
* <p>
* This type can be useful when you primarily want to utilize vehicle envelopes for traffic
* management.
* </p>
*/
LENGTH_IGNORED,
/**
* When releasing resources, the length of a vehicle is respected.
* <p>
* Only resources that are no longer "covered" by a vehicle (according to the length of the
* vehicle and the length of the paths behind it) are released. This is the "classic" way
* resources were managed before vehicle envelopes were introduced.
* </p>
*/
LENGTH_RESPECTED;
}
}

View File

@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import org.opentcs.access.Kernel;
import org.opentcs.access.LocalKernel;
import org.opentcs.components.kernel.KernelExtension;
import org.opentcs.components.kernel.services.InternalPlantModelService;
import org.opentcs.customizations.kernel.ActiveInAllModes;
import org.opentcs.customizations.kernel.KernelExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Initializes an openTCS kernel instance.
*/
public class KernelStarter {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(KernelStarter.class);
/**
* The kernel we're working with.
*/
private final LocalKernel kernel;
/**
* The plant model service.
*/
private final InternalPlantModelService plantModelService;
/**
* The kernel extensions to be registered.
*/
private final Set<KernelExtension> extensions;
/**
* The kernel's executor service.
*/
private final ScheduledExecutorService kernelExecutor;
/**
* Creates a new instance.
*
* @param kernel The kernel we're working with.
* @param plantModelService The plant model service.
* @param extensions The kernel extensions to be registered.
* @param kernelExecutor The kernel's executor service.
*/
@Inject
protected KernelStarter(
LocalKernel kernel,
InternalPlantModelService plantModelService,
@ActiveInAllModes
Set<KernelExtension> extensions,
@KernelExecutor
ScheduledExecutorService kernelExecutor
) {
this.kernel = requireNonNull(kernel, "kernel");
this.plantModelService = requireNonNull(plantModelService, "plantModelService");
this.extensions = requireNonNull(extensions, "extensions");
this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor");
}
/**
* Initializes the system and starts the openTCS kernel including modules.
*
* @throws IOException If there was a problem loading model data.
*/
public void startKernel()
throws IOException {
kernelExecutor.submit(() -> {
// Register kernel extensions.
for (KernelExtension extension : extensions) {
kernel.addKernelExtension(extension);
}
// Start local kernel.
kernel.initialize();
LOG.debug("Kernel initialized.");
plantModelService.loadPlantModel();
kernel.setState(Kernel.State.OPERATING);
});
}
}

View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import static java.util.Objects.requireNonNull;
import org.opentcs.access.Kernel.State;
import org.opentcs.components.Lifecycle;
import org.opentcs.kernel.persistence.ModelPersister;
import org.opentcs.kernel.workingset.PlantModelManager;
/**
* The abstract base class for classes that implement state specific kernel
* behaviour.
*/
public abstract class KernelState
implements
Lifecycle {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The model facade to the object pool.
*/
private final PlantModelManager plantModelManager;
/**
* The persister loading and storing model data.
*/
private final ModelPersister modelPersister;
/**
* Creates a new state.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param plantModelManager The plant model manager to be used.
* @param modelPersister The model persister to be used.
*/
public KernelState(
Object globalSyncObject,
PlantModelManager plantModelManager,
ModelPersister modelPersister
) {
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager");
this.modelPersister = requireNonNull(modelPersister, "modelPersister");
}
/**
* Returns the current state.
*
* @return The current state.
*/
public abstract State getState();
protected Object getGlobalSyncObject() {
return globalSyncObject;
}
protected ModelPersister getModelPersister() {
return modelPersister;
}
protected PlantModelManager getPlantModelManager() {
return plantModelManager;
}
}

View File

@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.Set;
import org.opentcs.access.Kernel;
import org.opentcs.components.kernel.KernelExtension;
import org.opentcs.customizations.kernel.ActiveInModellingMode;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.kernel.persistence.ModelPersister;
import org.opentcs.kernel.workingset.PlantModelManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements the standard openTCS kernel in modelling mode.
*/
public class KernelStateModelling
extends
KernelStateOnline {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(KernelStateModelling.class);
/**
* This kernel state's local extensions.
*/
private final Set<KernelExtension> extensions;
/**
* This instance's <em>initialized</em> flag.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param plantModelManager The plant model manager to be used.
* @param modelPersister The model persister to be used.
* @param configuration This class's configuration.
* @param extensions The kernel extensions to be used.
*/
@Inject
public KernelStateModelling(
@GlobalSyncObject
Object globalSyncObject,
PlantModelManager plantModelManager,
ModelPersister modelPersister,
KernelApplicationConfiguration configuration,
@ActiveInModellingMode
Set<KernelExtension> extensions
) {
super(
globalSyncObject,
plantModelManager,
modelPersister,
configuration.saveModelOnTerminateModelling()
);
this.extensions = requireNonNull(extensions, "extensions");
}
@Override
public void initialize() {
if (initialized) {
throw new IllegalStateException("Already initialized");
}
LOG.debug("Initializing modelling state...");
// Start kernel extensions.
for (KernelExtension extension : extensions) {
LOG.debug("Initializing kernel extension '{}'...", extension);
extension.initialize();
}
LOG.debug("Finished initializing kernel extensions.");
initialized = true;
LOG.debug("Modelling state initialized.");
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!initialized) {
throw new IllegalStateException("Not initialized, cannot terminate");
}
LOG.debug("Terminating modelling state...");
super.terminate();
// Terminate everything that may still use resources.
for (KernelExtension extension : extensions) {
LOG.debug("Terminating kernel extension '{}'...", extension);
extension.terminate();
}
LOG.debug("Terminated kernel extensions.");
initialized = false;
LOG.debug("Modelling state terminated.");
}
@Override
public Kernel.State getState() {
return Kernel.State.MODELLING;
}
}

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import org.opentcs.kernel.persistence.ModelPersister;
import org.opentcs.kernel.workingset.PlantModelManager;
/**
* The base class for the kernel's online states.
*/
public abstract class KernelStateOnline
extends
KernelState {
/**
* Whether to save the model when this state is terminated.
*/
private final boolean saveModelOnTerminate;
/**
* Creates a new instance.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param plantModelManager The plant model manager to be used.
* @param modelPersister The model persister to be used.
* @param saveModelOnTerminate Whether to save the model when this state is terminated.
*/
public KernelStateOnline(
Object globalSyncObject,
PlantModelManager plantModelManager,
ModelPersister modelPersister,
boolean saveModelOnTerminate
) {
super(globalSyncObject, plantModelManager, modelPersister);
this.saveModelOnTerminate = saveModelOnTerminate;
}
@Override
public void terminate() {
if (saveModelOnTerminate) {
savePlantModel();
}
}
private void savePlantModel()
throws IllegalStateException {
synchronized (getGlobalSyncObject()) {
getModelPersister().saveModel(getPlantModelManager().createPlantModelCreationTO());
}
}
}

View File

@@ -0,0 +1,340 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import static java.util.Objects.requireNonNull;
import com.google.common.util.concurrent.Uninterruptibles;
import jakarta.inject.Inject;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.opentcs.access.Kernel;
import org.opentcs.components.kernel.Dispatcher;
import org.opentcs.components.kernel.KernelExtension;
import org.opentcs.components.kernel.PeripheralJobDispatcher;
import org.opentcs.components.kernel.Router;
import org.opentcs.components.kernel.Scheduler;
import org.opentcs.components.kernel.services.InternalVehicleService;
import org.opentcs.customizations.kernel.ActiveInOperatingMode;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.customizations.kernel.KernelExecutor;
import org.opentcs.data.model.Vehicle;
import org.opentcs.kernel.extensions.controlcenter.vehicles.AttachmentManager;
import org.opentcs.kernel.peripherals.LocalPeripheralControllerPool;
import org.opentcs.kernel.peripherals.PeripheralAttachmentManager;
import org.opentcs.kernel.persistence.ModelPersister;
import org.opentcs.kernel.vehicles.LocalVehicleControllerPool;
import org.opentcs.kernel.workingset.PeripheralJobPoolManager;
import org.opentcs.kernel.workingset.PlantModelManager;
import org.opentcs.kernel.workingset.TransportOrderPoolManager;
import org.opentcs.kernel.workingset.WorkingSetCleanupTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements the standard openTCS kernel in normal operation.
*/
public class KernelStateOperating
extends
KernelStateOnline {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(KernelStateOperating.class);
/**
* The order pool manager.
*/
private final TransportOrderPoolManager orderPoolManager;
/**
* The job pool manager.
*/
private final PeripheralJobPoolManager jobPoolManager;
/**
* This kernel's router.
*/
private final Router router;
/**
* This kernel's scheduler.
*/
private final Scheduler scheduler;
/**
* This kernel's dispatcher.
*/
private final Dispatcher dispatcher;
/**
* This kernel's peripheral job dispatcher.
*/
private final PeripheralJobDispatcher peripheralJobDispatcher;
/**
* A pool of vehicle controllers.
*/
private final LocalVehicleControllerPool vehicleControllerPool;
/**
* A pool of peripheral controllers.
*/
private final LocalPeripheralControllerPool peripheralControllerPool;
/**
* The kernel's executor.
*/
private final ScheduledExecutorService kernelExecutor;
/**
* A task for periodically getting rid of old orders, order sequences and peripheral jobs.
*/
private final WorkingSetCleanupTask workingSetCleanupTask;
/**
* This kernel state's local extensions.
*/
private final Set<KernelExtension> extensions;
/**
* The kernel's attachment manager.
*/
private final AttachmentManager attachmentManager;
/**
* The kernel's peripheral attachment manager.
*/
private final PeripheralAttachmentManager peripheralAttachmentManager;
/**
* The vehicle service.
*/
private final InternalVehicleService vehicleService;
/**
* Listens to path lock events and updates the routing topology.
*/
private final PathLockEventListener pathLockListener;
/**
* Triggers dispatching of vehicles and transport orders on certain events.
*/
private final VehicleDispatchTrigger vehicleDispatchTrigger;
/**
* A handle for the cleaner task.
*/
private ScheduledFuture<?> cleanerTaskFuture;
/**
* This instance's <em>initialized</em> flag.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param globalSyncObject kernel threads' global synchronization object.
* @param plantModelManager The plant model manager to be used.
* @param orderPoolManager The order pool manager to be used.
* @param jobPoolManager The job pool manager to be used.
* @param modelPersister The model persister to be used.
* @param configuration This class's configuration.
* @param router The router to be used.
* @param scheduler The scheduler to be used.
* @param dispatcher The dispatcher to be used.
* @param peripheralJobDispatcher The peripheral job dispatcher to be used.
* @param controllerPool The vehicle controller pool to be used.
* @param peripheralControllerPool The peripheral controller pool to be used.
* @param kernelExecutor The kernel executer to be used.
* @param workingSetCleanupTask The workingset cleanup task to be used.
* @param extensions The kernel extensions to load.
* @param attachmentManager The attachment manager to be used.
* @param peripheralAttachmentManager The peripheral attachment manager to be used.
* @param vehicleService The vehicle service to be used.
* @param pathLockListener Listens to path lock events and updates the routing topology.
* @param vehicleDispatchTrigger Triggers dispatching of vehicles and transport orders on certain
* events.
*/
@Inject
public KernelStateOperating(
@GlobalSyncObject
Object globalSyncObject,
PlantModelManager plantModelManager,
TransportOrderPoolManager orderPoolManager,
PeripheralJobPoolManager jobPoolManager,
ModelPersister modelPersister,
KernelApplicationConfiguration configuration,
Router router,
Scheduler scheduler,
Dispatcher dispatcher,
PeripheralJobDispatcher peripheralJobDispatcher,
LocalVehicleControllerPool controllerPool,
LocalPeripheralControllerPool peripheralControllerPool,
@KernelExecutor
ScheduledExecutorService kernelExecutor,
WorkingSetCleanupTask workingSetCleanupTask,
@ActiveInOperatingMode
Set<KernelExtension> extensions,
AttachmentManager attachmentManager,
PeripheralAttachmentManager peripheralAttachmentManager,
InternalVehicleService vehicleService,
PathLockEventListener pathLockListener,
VehicleDispatchTrigger vehicleDispatchTrigger
) {
super(
globalSyncObject,
plantModelManager,
modelPersister,
configuration.saveModelOnTerminateOperating()
);
this.orderPoolManager = requireNonNull(orderPoolManager, "orderPoolManager");
this.jobPoolManager = requireNonNull(jobPoolManager, "jobPoolManager");
this.router = requireNonNull(router, "router");
this.scheduler = requireNonNull(scheduler, "scheduler");
this.dispatcher = requireNonNull(dispatcher, "dispatcher");
this.peripheralJobDispatcher = requireNonNull(
peripheralJobDispatcher,
"peripheralJobDispatcher"
);
this.vehicleControllerPool = requireNonNull(controllerPool, "controllerPool");
this.peripheralControllerPool = requireNonNull(
peripheralControllerPool,
"peripheralControllerPool"
);
this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor");
this.workingSetCleanupTask = requireNonNull(workingSetCleanupTask, "workingSetCleanupTask");
this.extensions = requireNonNull(extensions, "extensions");
this.attachmentManager = requireNonNull(attachmentManager, "attachmentManager");
this.peripheralAttachmentManager = requireNonNull(
peripheralAttachmentManager,
"peripheralAttachmentManager"
);
this.vehicleService = requireNonNull(vehicleService, "vehicleService");
this.pathLockListener = requireNonNull(pathLockListener, "pathLockListener");
this.vehicleDispatchTrigger = requireNonNull(vehicleDispatchTrigger, "vehicleDispatchTrigger");
}
// Implementation of interface Kernel starts here.
@Override
public void initialize() {
if (initialized) {
LOG.debug("Already initialized.");
return;
}
LOG.debug("Initializing operating state...");
// Reset vehicle states to ensure vehicles are not dispatchable initially.
for (Vehicle curVehicle : vehicleService.fetchObjects(Vehicle.class)) {
vehicleService.updateVehicleProcState(curVehicle.getReference(), Vehicle.ProcState.IDLE);
vehicleService.updateVehicleIntegrationLevel(
curVehicle.getReference(),
Vehicle.IntegrationLevel.TO_BE_RESPECTED
);
vehicleService.updateVehicleState(curVehicle.getReference(), Vehicle.State.UNKNOWN);
vehicleService.updateVehicleTransportOrder(curVehicle.getReference(), null);
vehicleService.updateVehicleOrderSequence(curVehicle.getReference(), null);
}
LOG.debug("Initializing scheduler '{}'...", scheduler);
scheduler.initialize();
LOG.debug("Initializing router '{}'...", router);
router.initialize();
LOG.debug("Initializing dispatcher '{}'...", dispatcher);
dispatcher.initialize();
LOG.debug("Initializing peripheral job dispatcher '{}'...", peripheralJobDispatcher);
peripheralJobDispatcher.initialize();
LOG.debug("Initializing vehicle controller pool '{}'...", vehicleControllerPool);
vehicleControllerPool.initialize();
LOG.debug("Initializing peripheral controller pool '{}'...", peripheralControllerPool);
peripheralControllerPool.initialize();
LOG.debug("Initializing attachment manager '{}'...", attachmentManager);
attachmentManager.initialize();
LOG.debug("Initializing peripheral attachment manager '{}'...", peripheralAttachmentManager);
peripheralAttachmentManager.initialize();
pathLockListener.initialize();
vehicleDispatchTrigger.initialize();
// Start a task for cleaning up old orders periodically.
cleanerTaskFuture = kernelExecutor.scheduleAtFixedRate(
workingSetCleanupTask,
workingSetCleanupTask.getSweepInterval(),
workingSetCleanupTask.getSweepInterval(),
TimeUnit.MILLISECONDS
);
// Start kernel extensions.
for (KernelExtension extension : extensions) {
LOG.debug("Initializing kernel extension '{}'...", extension);
extension.initialize();
}
LOG.debug("Finished initializing kernel extensions.");
initialized = true;
LOG.debug("Operating state initialized.");
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!initialized) {
LOG.debug("Not initialized.");
return;
}
LOG.debug("Terminating operating state...");
super.terminate();
// Terminate everything that may still use resources.
for (KernelExtension extension : extensions) {
LOG.debug("Terminating kernel extension '{}'...", extension);
extension.terminate();
}
LOG.debug("Terminated kernel extensions.");
// No need to clean up any more - it's all going to be cleaned up very soon.
cleanerTaskFuture.cancel(false);
cleanerTaskFuture = null;
// Terminate strategies.
LOG.debug("Terminating peripheral job dispatcher '{}'...", peripheralJobDispatcher);
peripheralJobDispatcher.terminate();
LOG.debug("Terminating dispatcher '{}'...", dispatcher);
dispatcher.terminate();
LOG.debug("Terminating router '{}'...", router);
router.terminate();
LOG.debug("Terminating scheduler '{}'...", scheduler);
scheduler.terminate();
LOG.debug("Terminating peripheral controller pool '{}'...", peripheralControllerPool);
peripheralControllerPool.terminate();
LOG.debug("Terminating vehicle controller pool '{}'...", vehicleControllerPool);
vehicleControllerPool.terminate();
LOG.debug("Terminating attachment manager '{}'...", attachmentManager);
attachmentManager.terminate();
LOG.debug("Terminating peripheral attachment manager '{}'...", peripheralAttachmentManager);
peripheralAttachmentManager.terminate();
pathLockListener.terminate();
vehicleDispatchTrigger.terminate();
// Grant communication adapters etc. some time to settle things.
Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS);
// Ensure that vehicles do not reference orders any more.
for (Vehicle curVehicle : vehicleService.fetchObjects(Vehicle.class)) {
vehicleService.updateVehicleProcState(curVehicle.getReference(), Vehicle.ProcState.IDLE);
vehicleService.updateVehicleIntegrationLevel(
curVehicle.getReference(),
Vehicle.IntegrationLevel.TO_BE_RESPECTED
);
vehicleService.updateVehicleState(curVehicle.getReference(), Vehicle.State.UNKNOWN);
vehicleService.updateVehicleTransportOrder(curVehicle.getReference(), null);
vehicleService.updateVehicleOrderSequence(curVehicle.getReference(), null);
}
// Remove all orders and order sequences from the pool.
orderPoolManager.clear();
// Remove all peripheral jobs from the pool.
jobPoolManager.clear();
initialized = false;
LOG.debug("Operating state terminated.");
}
@Override
public Kernel.State getState() {
return Kernel.State.OPERATING;
}
}

View File

@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import jakarta.inject.Inject;
import org.opentcs.access.Kernel;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.kernel.persistence.ModelPersister;
import org.opentcs.kernel.workingset.PlantModelManager;
/**
* This class implements the standard openTCS kernel when it's shut down.
*/
public class KernelStateShutdown
extends
KernelState {
/**
* Indicates whether this component is enabled.
*/
private boolean initialized;
/**
* Creates a new StandardKernelShutdownState.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param plantModelManager The plant model manager to be used.
* @param modelPersister The model persister to be used.
*/
@Inject
public KernelStateShutdown(
@GlobalSyncObject
Object globalSyncObject,
PlantModelManager plantModelManager,
ModelPersister modelPersister
) {
super(
globalSyncObject,
plantModelManager,
modelPersister
);
}
// Methods that HAVE to be implemented/overridden start here.
@Override
public void initialize() {
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
initialized = false;
}
@Override
public Kernel.State getState() {
return Kernel.State.SHUTDOWN;
}
}

View File

@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import org.opentcs.configuration.ConfigurationEntry;
import org.opentcs.configuration.ConfigurationPrefix;
import org.opentcs.kernel.workingset.WorkingSetCleanupTask;
/**
* Provides methods to configure the {@link WorkingSetCleanupTask}.
*/
@ConfigurationPrefix(OrderPoolConfiguration.PREFIX)
public interface OrderPoolConfiguration {
/**
* This configuration's prefix.
*/
String PREFIX = "orderpool";
@ConfigurationEntry(
type = "Long",
description = "The interval between sweeps (in ms).",
changesApplied = ConfigurationEntry.ChangesApplied.ON_NEW_PLANT_MODEL
)
long sweepInterval();
@ConfigurationEntry(
type = "Integer",
description = "The minimum age of orders or peripheral jobs to remove in a sweep (in ms).",
changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY
)
int sweepAge();
}

View File

@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.Set;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.DispatcherService;
import org.opentcs.components.kernel.services.RouterService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.TCSObjectEvent;
import org.opentcs.data.model.Path;
import org.opentcs.data.order.ReroutingType;
import org.opentcs.util.event.EventBus;
import org.opentcs.util.event.EventHandler;
/**
* Listens to path lock events and updates the routing topology.
*/
public class PathLockEventListener
implements
EventHandler,
Lifecycle {
/**
* The kernel configuration.
*/
private final KernelApplicationConfiguration configuration;
/**
* The router service.
*/
private final RouterService routerService;
/**
* The event bus.
*/
private final EventBus eventBus;
/**
* The dispatcher.
*/
private final DispatcherService dispatcher;
/**
* This instance's <em>initialized</em> flag.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param configuration The kernel configuration.
* @param routerService The router service.
* @param eventBus The event bus.
* @param dispatcher The dispatcher.
*/
@Inject
public PathLockEventListener(
KernelApplicationConfiguration configuration,
RouterService routerService,
@ApplicationEventBus
EventBus eventBus,
DispatcherService dispatcher
) {
this.configuration = requireNonNull(configuration, "configuration");
this.routerService = requireNonNull(routerService, "routerService");
this.eventBus = requireNonNull(eventBus, "eventBus");
this.dispatcher = requireNonNull(dispatcher, "dispatcher");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
initialized = true;
eventBus.subscribe(this);
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
initialized = false;
eventBus.unsubscribe(this);
}
@Override
public void onEvent(Object eventObject) {
if (!configuration.updateRoutingTopologyOnPathLockChange()) {
return;
}
if (!(eventObject instanceof TCSObjectEvent)) {
return;
}
TCSObjectEvent event = (TCSObjectEvent) eventObject;
if (hasPathLockChanged(event)) {
routerService.updateRoutingTopology(
Set.of(((Path) event.getCurrentObjectState()).getReference())
);
if (configuration.rerouteOnRoutingTopologyUpdate()) {
dispatcher.rerouteAll(ReroutingType.REGULAR);
}
}
}
private boolean hasPathLockChanged(TCSObjectEvent event) {
return event.getCurrentObjectState() instanceof Path
&& event.getType() == TCSObjectEvent.Type.OBJECT_MODIFIED
&& ((Path) event.getCurrentObjectState()).isLocked() != ((Path) event
.getPreviousObjectState()).isLocked();
}
}

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import org.opentcs.configuration.ConfigurationEntry;
import org.opentcs.configuration.ConfigurationPrefix;
/**
* Provides methods to configure the ssl connection.
*/
@ConfigurationPrefix(SslConfiguration.PREFIX)
public interface SslConfiguration {
/**
* This configuration's prefix.
*/
String PREFIX = "ssl";
@ConfigurationEntry(
type = "String",
description = {"The file url of the keystore."},
changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START,
orderKey = "0_connection_0"
)
String keystoreFile();
@ConfigurationEntry(
type = "String",
description = {"The password for the keystore."},
changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START,
orderKey = "0_connection_1"
)
String keystorePassword();
@ConfigurationEntry(
type = "String",
description = {"The file url of the truststore."},
changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START,
orderKey = "0_connection_2"
)
String truststoreFile();
@ConfigurationEntry(
type = "String",
description = {"The password for the truststore."},
changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START,
orderKey = "0_connection_3"
)
String truststorePassword();
}

View File

@@ -0,0 +1,229 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import static java.util.Objects.requireNonNull;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.inject.Provider;
import jakarta.inject.Inject;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.opentcs.access.Kernel;
import org.opentcs.access.Kernel.State;
import org.opentcs.access.KernelStateTransitionEvent;
import org.opentcs.access.LocalKernel;
import org.opentcs.components.kernel.KernelExtension;
import org.opentcs.components.kernel.services.NotificationService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.customizations.kernel.KernelExecutor;
import org.opentcs.data.notification.UserNotification;
import org.opentcs.util.event.EventBus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements the standard openTCS kernel.
*/
public class StandardKernel
implements
LocalKernel,
Runnable {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(StandardKernel.class);
/**
* A map to state providers used when switching kernel states.
*/
private final Map<Kernel.State, Provider<KernelState>> stateProviders;
/**
* The application's event bus.
*/
private final EventBus eventBus;
/**
* Our executor.
*/
private final ScheduledExecutorService kernelExecutor;
/**
* This kernel's order receivers.
*/
private final Set<KernelExtension> kernelExtensions = new HashSet<>();
/**
* Functions as a barrier for the kernel's {@link #run() run()} method.
*/
private final Semaphore terminationSemaphore = new Semaphore(0);
/**
* The notification service.
*/
private final NotificationService notificationService;
/**
* This kernel's <em>initialized</em> flag.
*/
private volatile boolean initialized;
/**
* The kernel implementing the actual functionality for the current mode.
*/
private KernelState kernelState;
/**
* Creates a new kernel.
*
* @param eventBus The central event bus to be used.
* @param kernelExecutor An executor for this kernel's tasks.
* @param stateProviders The state map to be used.
* @param notificationService The notification service to be used.
*/
@Inject
public StandardKernel(
@ApplicationEventBus
EventBus eventBus,
@KernelExecutor
ScheduledExecutorService kernelExecutor,
Map<Kernel.State, Provider<KernelState>> stateProviders,
NotificationService notificationService
) {
this.eventBus = requireNonNull(eventBus, "eventBus");
this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor");
this.stateProviders = requireNonNull(stateProviders, "stateProviders");
this.notificationService = requireNonNull(notificationService, "notificationService");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
// First of all, start all kernel extensions that are already registered.
for (KernelExtension extension : kernelExtensions) {
LOG.debug("Initializing extension: {}", extension.getClass().getName());
extension.initialize();
}
// Initial state is modelling.
setState(State.MODELLING);
initialized = true;
LOG.debug("Starting kernel thread");
Thread kernelThread = new Thread(this, "kernelThread");
kernelThread.start();
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
// Note that the actual shutdown of extensions should happen when the kernel
// thread (see run()) finishes, not here.
// Set the terminated flag and wake up this kernel's thread for termination.
initialized = false;
terminationSemaphore.release();
}
@Override
public void run() {
// Wait until terminated.
terminationSemaphore.acquireUninterruptibly();
LOG.info("Terminating...");
// Sleep a bit so clients have some time to receive an event for the
// SHUTDOWN state change and shut down gracefully themselves.
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
// Shut down all kernel extensions.
LOG.debug("Shutting down kernel extensions...");
for (KernelExtension extension : kernelExtensions) {
extension.terminate();
}
kernelExecutor.shutdown();
LOG.info("Kernel thread finished.");
}
@Override
public State getState() {
return kernelState.getState();
}
@Override
public void setState(State newState)
throws IllegalArgumentException {
requireNonNull(newState, "newState");
final Kernel.State oldState;
if (kernelState != null) {
oldState = kernelState.getState();
// Don't do anything if the new state is the same as the current one.
if (oldState == newState) {
LOG.debug("Already in state '{}', doing nothing.", newState.name());
return;
}
// Let listeners know we're in transition.
emitStateEvent(oldState, newState, false);
// Terminate previous state.
kernelState.terminate();
}
else {
oldState = null;
}
LOG.info("Switching kernel to state '{}'", newState.name());
switch (newState) {
case SHUTDOWN:
kernelState = stateProviders.get(Kernel.State.SHUTDOWN).get();
kernelState.initialize();
terminate();
break;
case MODELLING:
kernelState = stateProviders.get(Kernel.State.MODELLING).get();
kernelState.initialize();
break;
case OPERATING:
kernelState = stateProviders.get(Kernel.State.OPERATING).get();
kernelState.initialize();
break;
default:
throw new IllegalArgumentException("Unexpected state: " + newState);
}
emitStateEvent(oldState, newState, true);
notificationService.publishUserNotification(
new UserNotification(
"Kernel is now in state " + newState,
UserNotification.Level.INFORMATIONAL
)
);
}
@Override
public void addKernelExtension(final KernelExtension newExtension) {
requireNonNull(newExtension, "newExtension");
kernelExtensions.add(newExtension);
}
@Override
public void removeKernelExtension(final KernelExtension rmExtension) {
requireNonNull(rmExtension, "rmExtension");
kernelExtensions.remove(rmExtension);
}
// Methods not declared in any interface start here.
/**
* Generates an event for a state change.
*
* @param leftState The state left.
* @param enteredState The state entered.
* @param transitionFinished Whether the transition is finished or not.
*/
private void emitStateEvent(State leftState, State enteredState, boolean transitionFinished) {
eventBus.onEvent(new KernelStateTransitionEvent(leftState, enteredState, transitionFinished));
}
}

View File

@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.concurrent.Executor;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.DispatcherService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.customizations.kernel.KernelExecutor;
import org.opentcs.data.TCSObjectEvent;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.order.ReroutingType;
import org.opentcs.util.event.EventBus;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Triggers dispatching of vehicles and transport orders on certain events.
*/
public class VehicleDispatchTrigger
implements
EventHandler,
Lifecycle {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(VehicleDispatchTrigger.class);
/**
* The dispatcher in use.
*/
private final DispatcherService dispatcher;
/**
* The event bus.
*/
private final EventBus eventBus;
/**
* The app configuration.
*/
private final KernelApplicationConfiguration configuration;
/**
* The kernel executor.
*/
private final Executor kernelExecutor;
/**
* This instance's <em>initialized</em> flag.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param kernelExecutor The kernel executor to use.
* @param eventBus The event bus.
* @param dispatcher The dispatcher in use.
* @param configuration The application configuration.
*/
@Inject
public VehicleDispatchTrigger(
@KernelExecutor
Executor kernelExecutor,
@ApplicationEventBus
EventBus eventBus,
DispatcherService dispatcher,
KernelApplicationConfiguration configuration
) {
this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor");
this.eventBus = requireNonNull(eventBus, "eventBus");
this.dispatcher = requireNonNull(dispatcher, "dispatcher");
this.configuration = requireNonNull(configuration, "configuration");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
initialized = true;
eventBus.subscribe(this);
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
initialized = false;
eventBus.unsubscribe(this);
}
@Override
public void onEvent(Object event) {
if (!(event instanceof TCSObjectEvent)) {
return;
}
TCSObjectEvent objectEvent = (TCSObjectEvent) event;
if (objectEvent.getCurrentOrPreviousObjectState() instanceof Vehicle) {
checkVehicleChange(
(Vehicle) objectEvent.getPreviousObjectState(),
(Vehicle) objectEvent.getCurrentObjectState()
);
}
}
private void checkVehicleChange(Vehicle oldVehicle, Vehicle newVehicle) {
if (driveOrderFinished(oldVehicle, newVehicle)
&& configuration.rerouteOnDriveOrderFinished()) {
LOG.debug("Rerouting vehicle {}...", newVehicle);
dispatcher.reroute(newVehicle.getReference(), ReroutingType.REGULAR);
}
if ((newVehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_UTILIZED
|| newVehicle.getIntegrationLevel() == Vehicle.IntegrationLevel.TO_BE_RESPECTED)
&& (idleAndEnergyLevelChanged(oldVehicle, newVehicle)
|| awaitingNextOrder(oldVehicle, newVehicle)
|| orderSequenceNulled(oldVehicle, newVehicle))) {
LOG.debug("Dispatching for {}...", newVehicle);
// Dispatching may result in changes to the vehicle and thus trigger this code, which would
// then lead to a second dispatch run before the first one is completed. To avoid this, we
// ensure dispatching is done at some later point by scheduling it to be executed on the
// kernel executor (so it does not trigger itself in a loop).
kernelExecutor.execute(() -> dispatcher.dispatch());
}
}
private boolean idleAndEnergyLevelChanged(Vehicle oldVehicle, Vehicle newVehicle) {
// If the vehicle is idle and its energy level changed, we may want to order it to recharge.
return newVehicle.hasProcState(Vehicle.ProcState.IDLE)
&& (newVehicle.hasState(Vehicle.State.IDLE) || newVehicle.hasState(Vehicle.State.CHARGING))
&& newVehicle.getEnergyLevel() != oldVehicle.getEnergyLevel();
}
private boolean awaitingNextOrder(Vehicle oldVehicle, Vehicle newVehicle) {
// If the vehicle's processing state changed to IDLE or AWAITING_ORDER, it is waiting for
// its next order, so look for one.
return newVehicle.getProcState() != oldVehicle.getProcState()
&& (newVehicle.hasProcState(Vehicle.ProcState.IDLE)
|| newVehicle.hasProcState(Vehicle.ProcState.AWAITING_ORDER));
}
private boolean orderSequenceNulled(Vehicle oldVehicle, Vehicle newVehicle) {
// If the vehicle's order sequence reference has become null, the vehicle has just been released
// from an order sequence, so we may look for new assignments.
return newVehicle.getOrderSequence() == null
&& oldVehicle.getOrderSequence() != null;
}
private boolean driveOrderFinished(Vehicle oldVehicle, Vehicle newVehicle) {
// If the vehicle's processing state changes to AWAITING_ORDER, it has finished its current
// drive order.
return newVehicle.getProcState() != oldVehicle.getProcState()
&& newVehicle.hasProcState(Vehicle.ProcState.AWAITING_ORDER);
}
}

View File

@@ -0,0 +1,393 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.controlcenter.vehicles;
import static java.util.Objects.requireNonNull;
import com.google.common.base.Strings;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.VehicleCommAdapter;
import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription;
import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory;
import org.opentcs.drivers.vehicle.management.ProcessModelEvent;
import org.opentcs.drivers.vehicle.management.VehicleAttachmentEvent;
import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation;
import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO;
import org.opentcs.kernel.KernelApplicationConfiguration;
import org.opentcs.kernel.vehicles.LocalVehicleControllerPool;
import org.opentcs.kernel.vehicles.VehicleCommAdapterRegistry;
import org.opentcs.util.Assertions;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages attachment and detachment of communication adapters to vehicles.
*/
public class AttachmentManager
implements
Lifecycle {
/**
* This class's logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(AttachmentManager.class);
/**
* This class's configuration.
*/
private final KernelApplicationConfiguration configuration;
/**
* The object service.
*/
private final TCSObjectService objectService;
/**
* The vehicle controller pool.
*/
private final LocalVehicleControllerPool controllerPool;
/**
* The comm adapter registry.
*/
private final VehicleCommAdapterRegistry commAdapterRegistry;
/**
* The pool of vehicle entries.
*/
private final VehicleEntryPool vehicleEntryPool;
/**
* The handler to send events to.
*/
private final EventHandler eventHandler;
/**
* The pool of comm adapter attachments.
*/
private final Map<String, VehicleAttachmentInformation> attachmentPool = new HashMap<>();
/**
* Whether the attachment manager is initialized or not.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param objectService The object service.
* @param controllerPool The vehicle controller pool.
* @param commAdapterRegistry The comm adapter registry.
* @param vehicleEntryPool The pool of vehicle entries.
* @param eventHandler The handler to send events to.
* @param configuration This class's configuration.
*/
@Inject
public AttachmentManager(
@Nonnull
TCSObjectService objectService,
@Nonnull
LocalVehicleControllerPool controllerPool,
@Nonnull
VehicleCommAdapterRegistry commAdapterRegistry,
@Nonnull
VehicleEntryPool vehicleEntryPool,
@Nonnull
@ApplicationEventBus
EventHandler eventHandler,
@Nonnull
KernelApplicationConfiguration configuration
) {
this.objectService = requireNonNull(objectService, "objectService");
this.controllerPool = requireNonNull(controllerPool, "controllerPool");
this.commAdapterRegistry = requireNonNull(commAdapterRegistry, "commAdapterRegistry");
this.vehicleEntryPool = requireNonNull(vehicleEntryPool, "vehicleEntryPool");
this.eventHandler = requireNonNull(eventHandler, "eventHandler");
this.configuration = requireNonNull(configuration, "configuration");
}
@Override
public void initialize() {
if (isInitialized()) {
LOG.debug("Already initialized.");
return;
}
commAdapterRegistry.initialize();
vehicleEntryPool.initialize();
initAttachmentPool();
autoAttachAllAdapters();
if (configuration.autoEnableDriversOnStartup()) {
autoEnableAllAdapters();
}
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
LOG.debug("Not initialized.");
return;
}
// Detach all attached drivers to clean up.
detachAllAdapters();
vehicleEntryPool.terminate();
commAdapterRegistry.terminate();
initialized = false;
}
/**
* Attaches an adapter to a vehicle.
*
* @param vehicleName The vehicle name.
* @param factory The factory that provides the adapter to be assigned.
*/
public void attachAdapterToVehicle(
@Nonnull
String vehicleName,
@Nonnull
VehicleCommAdapterFactory factory
) {
requireNonNull(vehicleName, "vehicleName");
requireNonNull(factory, "factory");
LOG.info(
"Attaching vehicle comm adapter: '{}' -- '{}'...",
vehicleName,
factory.getClass().getName()
);
VehicleEntry vehicleEntry = vehicleEntryPool.getEntryFor(vehicleName);
if (vehicleEntry == null) {
LOG.warn(
"No vehicle entry found for '{}'. Entries: {}",
vehicleName,
vehicleEntryPool
);
return;
}
VehicleCommAdapter commAdapter = factory.getAdapterFor(vehicleEntry.getVehicle());
if (commAdapter == null) {
LOG.warn(
"Factory {} did not provide adapter for vehicle {}, ignoring.",
factory,
vehicleEntry.getVehicle().getName()
);
return;
}
// Perform a cleanup for the old adapter.
disableAndTerminateAdapter(vehicleEntry);
controllerPool.detachVehicleController(vehicleEntry.getVehicle().getName());
commAdapter.initialize();
controllerPool.attachVehicleController(vehicleEntry.getVehicle().getName(), commAdapter);
vehicleEntry.setCommAdapterFactory(factory);
vehicleEntry.setCommAdapter(commAdapter);
vehicleEntry.setProcessModel(commAdapter.getProcessModel());
objectService.updateObjectProperty(
vehicleEntry.getVehicle().getReference(),
Vehicle.PREFERRED_ADAPTER,
factory.getClass().getName()
);
updateAttachmentInformation(vehicleEntry);
}
/**
* Automatically attach a vehicle to an adapter.
*
* @param vehicleName The name of the vehicle to attach.
*/
public void autoAttachAdapterToVehicle(
@Nonnull
String vehicleName
) {
requireNonNull(vehicleName, "vehicleName");
VehicleEntry vehicleEntry = vehicleEntryPool.getEntryFor(vehicleName);
if (vehicleEntry == null) {
LOG.warn(
"No vehicle entry found for '{}'. Entries: {}",
vehicleName,
vehicleEntryPool
);
return;
}
// Do not auto-attach if there is already a comm adapter attached to the vehicle.
if (vehicleEntry.getCommAdapter() != null) {
return;
}
Vehicle vehicle = getUpdatedVehicle(vehicleEntry.getVehicle());
String prefAdapter = vehicle.getProperties().get(Vehicle.PREFERRED_ADAPTER);
VehicleCommAdapterFactory factory = findFactoryWithName(prefAdapter);
if (factory != null && factory.providesAdapterFor(vehicle)) {
attachAdapterToVehicle(vehicleName, factory);
}
else {
if (!Strings.isNullOrEmpty(prefAdapter)) {
LOG.warn(
"Couldn't attach preferred adapter {} to {}. Attaching first available adapter.",
prefAdapter,
vehicleEntry.getVehicle().getName()
);
}
List<VehicleCommAdapterFactory> factories
= commAdapterRegistry.findFactoriesFor(vehicleEntry.getVehicle());
if (!factories.isEmpty()) {
attachAdapterToVehicle(vehicleName, factories.get(0));
}
}
}
/**
* Automatically attach all vehicles to an adapter.
*/
public void autoAttachAllAdapters() {
vehicleEntryPool.getEntries().forEach((vehicleName, entry) -> {
autoAttachAdapterToVehicle(vehicleName);
});
}
/**
* Returns the attachment information for a vehicle.
*
* @param vehicleName The name of the vehicle.
* @return Attachment information about the vehicle.
*/
public VehicleAttachmentInformation getAttachmentInformation(String vehicleName) {
requireNonNull(vehicleName, "vehicleName");
Assertions.checkArgument(
attachmentPool.get(vehicleName) != null,
"No attachment information for vehicle %s",
vehicleName
);
return attachmentPool.get(vehicleName);
}
public Map<String, VehicleAttachmentInformation> getAttachmentPool() {
return attachmentPool;
}
private void disableAndTerminateAdapter(
@Nonnull
VehicleEntry vehicleEntry
) {
requireNonNull(vehicleEntry, "vehicleEntry");
VehicleCommAdapter commAdapter = vehicleEntry.getCommAdapter();
if (commAdapter != null) {
commAdapter.disable();
// Let the adapter know cleanup time is here.
commAdapter.terminate();
}
}
private void initAttachmentPool() {
vehicleEntryPool.getEntries().forEach((vehicleName, entry) -> {
List<VehicleCommAdapterDescription> availableCommAdapters
= commAdapterRegistry.getFactories().stream()
.filter(f -> f.providesAdapterFor(entry.getVehicle()))
.map(f -> f.getDescription())
.collect(Collectors.toList());
attachmentPool.put(
vehicleName,
new VehicleAttachmentInformation(
entry.getVehicle().getReference(),
availableCommAdapters,
new NullVehicleCommAdapterDescription()
)
);
});
}
private void updateAttachmentInformation(VehicleEntry entry) {
String vehicleName = entry.getVehicleName();
VehicleCommAdapterFactory factory = entry.getCommAdapterFactory();
VehicleAttachmentInformation newAttachment = attachmentPool.get(vehicleName)
.withAttachedCommAdapter(factory.getDescription());
attachmentPool.put(vehicleName, newAttachment);
eventHandler.onEvent(new VehicleAttachmentEvent(vehicleName, newAttachment));
if (entry.getCommAdapter() == null) {
// In case we are detached
eventHandler.onEvent(new ProcessModelEvent(vehicleName, new VehicleProcessModelTO()));
}
else {
eventHandler.onEvent(
new ProcessModelEvent(
vehicleName,
entry.getCommAdapter()
.createTransferableProcessModel()
)
);
}
}
/**
* Returns a fresh copy of a vehicle from the kernel.
*
* @param vehicle The old vehicle instance.
* @return The fresh vehicle instance.
*/
private Vehicle getUpdatedVehicle(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle, "vehicle");
return objectService.fetchObjects(Vehicle.class).stream()
.filter(updatedVehicle -> Objects.equals(updatedVehicle.getName(), vehicle.getName()))
.findFirst().orElse(vehicle);
}
private void autoEnableAllAdapters() {
vehicleEntryPool.getEntries().values().stream()
.map(entry -> entry.getCommAdapter())
.filter(adapter -> adapter != null)
.filter(adapter -> !adapter.isEnabled())
.forEach(adapter -> adapter.enable());
}
private void detachAllAdapters() {
LOG.debug("Detaching vehicle communication adapters...");
vehicleEntryPool.getEntries().forEach((vehicleName, entry) -> {
disableAndTerminateAdapter(entry);
});
LOG.debug("Detached vehicle communication adapters");
}
@Nullable
private VehicleCommAdapterFactory findFactoryWithName(
@Nullable
String name
) {
return commAdapterRegistry.getFactories().stream()
.filter(factory -> factory.getClass().getName().equals(name))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.controlcenter.vehicles;
import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription;
/**
* A {@link VehicleCommAdapterDescription} for no comm adapter.
*/
public class NullVehicleCommAdapterDescription
extends
VehicleCommAdapterDescription {
/**
* Creates a new instance.
*/
public NullVehicleCommAdapterDescription() {
}
@Override
public String getDescription() {
return "-";
}
@Override
public boolean isSimVehicleCommAdapter() {
return false;
}
}

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.controlcenter.vehicles;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.VehicleCommAdapter;
import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription;
import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory;
/**
* A Vehicle adapter factory that creates no vehicles adapters.
*/
public class NullVehicleCommAdapterFactory
implements
VehicleCommAdapterFactory {
/**
* Creates a new instance.
*/
public NullVehicleCommAdapterFactory() {
}
@Override
public VehicleCommAdapterDescription getDescription() {
return new NullVehicleCommAdapterDescription();
}
@Override
public boolean providesAdapterFor(Vehicle vehicle) {
return false;
}
@Override
public VehicleCommAdapter getAdapterFor(Vehicle vehicle) {
return null;
}
@Override
public void initialize() {
}
@Override
public boolean isInitialized() {
return true;
}
@Override
public void terminate() {
}
}

View File

@@ -0,0 +1,193 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.controlcenter.vehicles;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.VehicleCommAdapter;
import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory;
import org.opentcs.drivers.vehicle.VehicleProcessModel;
/**
* Represents a {@link Vehicle} in the {@link VehicleEntryPool}.
*/
public class VehicleEntry
implements
PropertyChangeListener {
/**
* The vehicle this entry represents.
*/
private final Vehicle vehicle;
/**
* Used for implementing property change events.
*/
@SuppressWarnings("this-escape")
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
/**
* The process model for the vehicle.
*/
private VehicleProcessModel processModel;
/**
* The comm adapter factory for this vehicle.
*/
private VehicleCommAdapterFactory commAdapterFactory = new NullVehicleCommAdapterFactory();
/**
* The comm adapter that is attached to this vehicle.
*/
private VehicleCommAdapter commAdapter;
/**
* Creates a vehicle entry.
*
* @param vehicle The vehicle this entry represents.
*/
public VehicleEntry(Vehicle vehicle) {
this.vehicle = requireNonNull(vehicle, "vehicle");
this.processModel = new VehicleProcessModel(vehicle);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getSource() instanceof VehicleProcessModel)) {
return;
}
pcs.firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
}
/**
* Add a property change listener.
*
* @param listener The listener to add.
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
/**
* Remove a property change listener.
*
* @param listener The listener to remove.
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
/**
* Returns the vehicle that is represented by this entry.
*
* @return The vehicle that is represented by this entry.
*/
@Nonnull
public Vehicle getVehicle() {
return vehicle;
}
/**
* Returns the name of the vehicle that is represented by this entry.
*
* @return The name of the vehicle that is represented by this entry.
*/
@Nonnull
public String getVehicleName() {
return vehicle.getName();
}
/**
* Returns the process model of the vehicle that is represented by this entry.
*
* @return The process model of the vehicle that is represented by this entry.
*/
@Nonnull
public VehicleProcessModel getProcessModel() {
return processModel;
}
/**
* Sets the process model for the vehicle represented by this entry.
*
* @param processModel The new process model for the vehicle.
*/
public void setProcessModel(
@Nonnull
VehicleProcessModel processModel
) {
VehicleProcessModel oldProcessModel = this.processModel;
this.processModel = requireNonNull(processModel, "processModel");
oldProcessModel.removePropertyChangeListener(this);
processModel.addPropertyChangeListener(this);
pcs.firePropertyChange(Attribute.PROCESS_MODEL.name(), oldProcessModel, processModel);
}
@Nonnull
public VehicleCommAdapterFactory getCommAdapterFactory() {
return commAdapterFactory;
}
/**
* Sets the comm adapter factory for this entry.
*
* @param commAdapterFactory The new comm adapter factory.
*/
public void setCommAdapterFactory(
@Nonnull
VehicleCommAdapterFactory commAdapterFactory
) {
VehicleCommAdapterFactory oldValue = this.commAdapterFactory;
this.commAdapterFactory = commAdapterFactory;
pcs.firePropertyChange(Attribute.COMM_ADAPTER_FACTORY.name(), oldValue, commAdapterFactory);
}
/**
* Returns the comm adapter factory for this entry.
*
* @return The comm adapter factory for this entry
*/
@Nullable
public VehicleCommAdapter getCommAdapter() {
return commAdapter;
}
/**
* Sets the comm adapter for this entry.
*
* @param commAdapter The new comm adapter.
*/
public void setCommAdapter(
@Nullable
VehicleCommAdapter commAdapter
) {
VehicleCommAdapter oldValue = this.commAdapter;
this.commAdapter = commAdapter;
pcs.firePropertyChange(Attribute.COMM_ADAPTER.name(), oldValue, commAdapter);
}
/**
* Enum elements used as notification arguments to specify which argument changed.
*/
public enum Attribute {
/**
* Indicates a change of the process model reference.
*/
PROCESS_MODEL,
/**
* Indicates a change of the comm adapter factory reference.
*/
COMM_ADAPTER_FACTORY,
/**
* Indicates a change of the comm adapter reference.
*/
COMM_ADAPTER
}
}

View File

@@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.controlcenter.vehicles;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import java.util.Map;
import java.util.TreeMap;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.data.model.Vehicle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides a pool of {@link VehicleEntry}s with an entry for every {@link Vehicle} object in the
* kernel.
*/
public class VehicleEntryPool
implements
Lifecycle {
/**
* This class's logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(VehicleEntryPool.class);
/**
* The object service.
*/
private final TCSObjectService objectService;
/**
* The entries of this pool.
*/
private final Map<String, VehicleEntry> entries = new TreeMap<>();
/**
* Whether the pool is initialized or not.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param objectService The object service.
*/
@Inject
public VehicleEntryPool(
@Nonnull
TCSObjectService objectService
) {
this.objectService = requireNonNull(objectService, "objectService");
}
@Override
public void initialize() {
if (isInitialized()) {
LOG.debug("Already initialized.");
return;
}
objectService.fetchObjects(Vehicle.class).stream()
.forEach(vehicle -> entries.put(vehicle.getName(), new VehicleEntry(vehicle)));
LOG.debug("Initialized vehicle entry pool: {}", entries);
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
LOG.debug("Not initialized.");
return;
}
entries.clear();
initialized = false;
}
/**
* Returns all entries in the pool.
*
* @return Map of vehicle names to their vehicle entries.
*/
@Nonnull
public Map<String, VehicleEntry> getEntries() {
return entries;
}
/**
* Returns the {@link VehicleEntry} for the given vehicle name.
*
* @param vehicleName The vehicle name to get the entry for.
* @return the vehicle entry for the given vehicle name.
*/
@Nullable
public VehicleEntry getEntryFor(
@Nonnull
String vehicleName
) {
requireNonNull(vehicleName, "vehicleName");
return entries.get(vehicleName);
}
}

View File

@@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
/**
* Classes of the kernel control center concerning vehicles/vehicle drivers.
* The GUI parts of the driver framework.
*/
package org.opentcs.kernel.extensions.controlcenter.vehicles;

View File

@@ -0,0 +1,232 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.watchdog;
import static java.util.Objects.requireNonNull;
import static org.opentcs.data.model.Vehicle.IntegrationLevel.TO_BE_RESPECTED;
import static org.opentcs.data.model.Vehicle.IntegrationLevel.TO_BE_UTILIZED;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.NotificationService;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.customizations.kernel.KernelExecutor;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Block;
import org.opentcs.data.model.Point;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.notification.UserNotification;
/**
* Periodically checks the occupancy status of single-vehicle blocks.
*
* This check will publish a user notification if a single-vehicle block is occupied by more than
* one vehicle.
* A single notification will be published when the violation is first detected.
* A second notification will be published when the violation changed or is resolved.
*
* The exact rules for when a violation notification should be sent are:
*
* <ul>
* <li> A violation should trigger a notification if a block is occupied by more than one vehicle
* and the set of vehicles occupying the block is different to the one in the previous iteration.
* The order of the occupants does not matter.
* (V1, V2) is the same as (V2, V1).</li>
* <li> If a block was previously occupied by more than one vehicle and is now occupied by one or no
* vehicle, a notification about the resultion of the situation is sent.</li>
* </ul>
*
* Examples:
*
* <ul>
* <li> A block previously occupied by (V1, V2) that is now occupied by (V1, V2, V3) should
* trigger a new violation notification.</li>
* <li> A block previously occupied by (V1, V2) that is now occupied by (V1) should trigger a
* resolution notification.</li>
* <li> A block previously occupied by (V1, V2) that is now still occupied by (V1, V2) or (V2, V1)
* should not trigger a new notification.</li>
* </ul>
*/
public class BlockConsistencyCheck
implements
Runnable,
Lifecycle {
/**
* Notification source.
*/
private static final String NOTIFICATION_SOURCE = "Watchdog - Block consistency check";
/**
* Object service to access the model.
*/
private final TCSObjectService objectService;
/**
* The service to send out user notifications.
*/
private final NotificationService notificationService;
/**
* The kernel executor.
*/
private final ScheduledExecutorService kernelExecutor;
/**
* The configuration.
*/
private final WatchdogConfiguration configuration;
/**
* Whether this check is initialized.
*/
private boolean initialized;
/**
* The Future created for the block check task.
*/
private ScheduledFuture<?> scheduledFuture;
/**
* Holds currently known block occupations.
* Maps a block reference to a set of vehicles contained in that block.
*/
private Map<TCSResourceReference<Block>, Set<TCSObjectReference<Vehicle>>> occupations
= new HashMap<>();
/**
* Creates a new instance.
*
* @param kernelExecutor The kernel executor.
* @param objectService The object service.
* @param notificationService The notification service.
* @param configuration The watchdog configuration.
*/
@Inject
public BlockConsistencyCheck(
@KernelExecutor
ScheduledExecutorService kernelExecutor,
TCSObjectService objectService,
NotificationService notificationService,
WatchdogConfiguration configuration
) {
this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor");
this.objectService = requireNonNull(objectService, "objectService");
this.notificationService = requireNonNull(notificationService, "notificationService");
this.configuration = requireNonNull(configuration, "configuration");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
scheduledFuture = kernelExecutor.scheduleAtFixedRate(
this,
configuration.blockConsistencyCheckInterval(),
configuration.blockConsistencyCheckInterval(),
TimeUnit.MILLISECONDS
);
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
scheduledFuture = null;
}
initialized = false;
}
@Override
public void run() {
Map<TCSResourceReference<Block>, Set<TCSObjectReference<Vehicle>>> currentOccupations
= findCurrentOccupations();
// Find new violations.
currentOccupations.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.filter(entry -> {
return !occupations.containsKey(entry.getKey())
|| !occupations.get(entry.getKey()).equals(entry.getValue());
})
.forEach(entry -> {
notificationService.publishUserNotification(
new UserNotification(
NOTIFICATION_SOURCE,
String.format(
"Block %s is overfull. Occupied by vehicles: %s",
entry.getKey().getName(),
entry.getValue().stream()
.map(vehicle -> vehicle.getName())
.collect(Collectors.joining(", "))
),
UserNotification.Level.IMPORTANT
)
);
});
// Find resolved violations
occupations.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.filter(entry -> {
return !currentOccupations.containsKey(entry.getKey())
|| currentOccupations.get(entry.getKey()).size() <= 1;
})
.forEach(entry -> {
notificationService.publishUserNotification(
new UserNotification(
NOTIFICATION_SOURCE,
String.format("Block %s is not overfull any more.", entry.getKey().getName()),
UserNotification.Level.IMPORTANT
)
);
});
occupations = currentOccupations;
}
private Map<TCSResourceReference<Block>, Set<TCSObjectReference<Vehicle>>>
findCurrentOccupations() {
Map<TCSResourceReference<Block>, Set<TCSObjectReference<Vehicle>>> currentOccupations
= new HashMap<>();
Set<Block> blocks = objectService.fetchObjects(Block.class);
objectService.fetchObjects(Vehicle.class)
.stream()
.filter(vehicle -> {
return vehicle.getIntegrationLevel() == TO_BE_RESPECTED
|| vehicle.getIntegrationLevel() == TO_BE_UTILIZED;
})
.filter(vehicle -> vehicle.getCurrentPosition() != null)
.forEach(vehicle -> {
Point currentPoint = objectService.fetchObject(Point.class, vehicle.getCurrentPosition());
blocks.stream()
.filter(block -> block.getType() == Block.Type.SINGLE_VEHICLE_ONLY)
.filter(block -> block.getMembers().contains(currentPoint.getReference()))
.forEach(block -> {
currentOccupations.putIfAbsent(block.getReference(), new HashSet<>());
currentOccupations.get(block.getReference()).add(vehicle.getReference());
});
});
return currentOccupations;
}
}

View File

@@ -0,0 +1,179 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.watchdog;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.NotificationService;
import org.opentcs.customizations.kernel.KernelExecutor;
import org.opentcs.data.notification.UserNotification;
import org.opentcs.kernel.extensions.watchdog.StrandedVehicles.VehicleSnapshot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Checks for vehicles that are stranded for too long.
* <p>
* A vehicle is considered "stranded" if one of the following conditions applies and lasts longer
* than a configurable time period (see
* {@link WatchdogConfiguration#strandedVehicleDurationThreshold()}):
* </p>
* <ul>
* <li>The vehicle is idle and at a position that is <em>not</em> a parking position.</li>
* <li>The vehicle is idle and has a transport order assigned to it.</li>
* </ul>
* This check publishes a user notification when a vehicle is considered stranded and another user
* notification once a vehicle is no longer considered stranded.
*/
public class StrandedVehicleCheck
implements
Runnable,
Lifecycle {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(StrandedVehicleCheck.class);
/**
* Source for notifications.
*/
private static final String NOTIFICATION_SOURCE = "Watchdog - Stranded vehicle check";
/**
* A formatter for timestamps.
*/
private static final DateTimeFormatter TIMESTAMP_FORMATTER
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
/**
* The service to send out user notifications.
*/
private final NotificationService notificationService;
/**
* The kernel executor.
*/
private final ScheduledExecutorService kernelExecutor;
/**
* The configuration.
*/
private final WatchdogConfiguration configuration;
/**
* Keeps track of stranded vehicles.
*/
private final StrandedVehicles stranded;
/**
* Whether this check is initialized.
*/
private boolean initialized;
/**
* The Future created for the block check task.
*/
private ScheduledFuture<?> scheduledFuture;
/**
* Creates a new instance.
*
* @param kernelExecutor The kernel executor.
* @param notificationService The notification service.
* @param configuration The watchdog configuration.
* @param stranded Keeps track of stranded vehicles.
*/
@Inject
public StrandedVehicleCheck(
@KernelExecutor
ScheduledExecutorService kernelExecutor,
NotificationService notificationService,
WatchdogConfiguration configuration,
StrandedVehicles stranded
) {
this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor");
this.notificationService = requireNonNull(notificationService, "notificationService");
this.configuration = requireNonNull(configuration, "configuration");
this.stranded = requireNonNull(stranded, "stranded");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
scheduledFuture = kernelExecutor.scheduleAtFixedRate(
this,
configuration.strandedVehicleCheckInterval(),
configuration.strandedVehicleCheckInterval(),
TimeUnit.MILLISECONDS
);
stranded.initialize();
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
scheduledFuture = null;
}
stranded.terminate();
initialized = false;
}
@Override
public void run() {
long currentTime = System.currentTimeMillis();
long strandedTimeThreshold = configuration.strandedVehicleDurationThreshold();
stranded.identifyStrandedVehicles(currentTime, strandedTimeThreshold);
Set<VehicleSnapshot> newlyStrandedVehicles = stranded.newlyStrandedVehicles();
Set<VehicleSnapshot> noLongerStrandedVehicles = stranded.noLongerStrandedVehicles();
newlyStrandedVehicles
.forEach(vehicleSnapshot -> {
notificationService.publishUserNotification(
new UserNotification(
NOTIFICATION_SOURCE,
String.format(
"Vehicle '%s' is stranded since: %s",
vehicleSnapshot.getVehicle().getName(),
TIMESTAMP_FORMATTER.format(
Instant.ofEpochMilli(vehicleSnapshot.getLastRelevantStateChange())
)
),
UserNotification.Level.INFORMATIONAL
)
);
});
noLongerStrandedVehicles
.forEach(vehicleSnapshot -> {
notificationService.publishUserNotification(
new UserNotification(
NOTIFICATION_SOURCE,
String.format(
"Vehicle '%s' is no longer stranded.",
vehicleSnapshot.getVehicle().getName()
),
UserNotification.Level.INFORMATIONAL
)
);
});
}
}

View File

@@ -0,0 +1,267 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.watchdog;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Point;
import org.opentcs.data.model.Vehicle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class to find out stranded vehicles.
*/
public class StrandedVehicles
implements
Lifecycle {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(StrandedVehicles.class);
/**
* Object service to access the model.
*/
private final TCSObjectService objectService;
/**
* Provider to get the current time.
*/
private final TimeProvider timeProvider;
/**
* Map to store the current snapshot for each vehicle.
*/
private final Map<String, VehicleSnapshot> currentSnapshots = new HashMap<>();
/**
* Map to store the previous snapshot for each vehicle.
*/
private final Map<String, VehicleSnapshot> previousSnapshots = new HashMap<>();
/**
* Whether this instance is initialized.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param objectService The object service.
* @param timeProvider Provider to get the current time.
*/
@Inject
public StrandedVehicles(
TCSObjectService objectService,
TimeProvider timeProvider
) {
this.objectService = requireNonNull(objectService, "objectService");
this.timeProvider = requireNonNull(timeProvider, "timeProvider");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
long currentTime = timeProvider.getCurrentTime();
objectService.fetchObjects(Vehicle.class).forEach(vehicle -> {
VehicleSnapshot vehicleSnapshot = new VehicleSnapshot(vehicle);
vehicleSnapshot.setLastRelevantStateChange(currentTime);
currentSnapshots.put(vehicle.getName(), vehicleSnapshot);
});
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
currentSnapshots.clear();
previousSnapshots.clear();
initialized = false;
}
/**
* Identifies stranded vehicles.
*
* @param currentTime The current time.
* @param strandedDurationThreshold The duration that a vehicle must be in a stranded state to
* actually be considered stranded.
*/
public void identifyStrandedVehicles(
long currentTime,
long strandedDurationThreshold
) {
LOG.debug("Identifying stranded vehicles...");
previousSnapshots.clear();
previousSnapshots.putAll(currentSnapshots);
currentSnapshots.clear();
objectService.fetchObjects(Vehicle.class)
.stream()
.forEach(vehicle -> {
VehicleSnapshot previousSnapshot = previousSnapshots.get(vehicle.getName());
VehicleSnapshot currentSnapshot = new VehicleSnapshot(vehicle);
if (vehicle.getState() != Vehicle.State.IDLE
|| relevantPropertiesChanged(previousSnapshot, currentSnapshot)) {
currentSnapshot.setLastRelevantStateChange(currentTime);
currentSnapshot.setStranded(false);
}
else if (isInStrandedState(vehicle)) {
LOG.debug("Checking if vehicle '{}' is stranded long enough...", vehicle);
long lastRelevantStateChange = previousSnapshot.getLastRelevantStateChange();
currentSnapshot.setLastRelevantStateChange(lastRelevantStateChange);
long strandedDuration = currentTime - lastRelevantStateChange;
currentSnapshot.setStranded(strandedDuration >= strandedDurationThreshold);
}
else {
// The state of the vehicle has not effectively changed. Therefore, apply values from
// the previous snapshot.
currentSnapshot.setLastRelevantStateChange(
previousSnapshot.getLastRelevantStateChange()
);
currentSnapshot.setStranded(previousSnapshot.isStranded());
}
LOG.debug("Snapshot of vehicle '{}': {}", vehicle.getName(), currentSnapshot);
currentSnapshots.put(vehicle.getName(), currentSnapshot);
});
}
/**
* Returns vehicles that are currently considered newly stranded (i.e., vehicles that were
* previously not considered stranded, but are now considered stranded).
*
* @return The set of vehicles that are considered newly stranded.
*/
public Set<VehicleSnapshot> newlyStrandedVehicles() {
return currentSnapshots.values().stream()
.filter(
vehicleSnapshot -> !previousSnapshots.get(vehicleSnapshot.getVehicle().getName())
.isStranded()
&& vehicleSnapshot.isStranded()
)
.collect(Collectors.toSet());
}
/**
* Returns vehicles that are currently considered no longer stranded (i.e., vehicles that were
* previously considered stranded, but are now not considered stranded).
*
* @return The set of vehicles that are no longer considered stranded.
*/
public Set<VehicleSnapshot> noLongerStrandedVehicles() {
return currentSnapshots.values().stream()
.filter(
vehicleSnapshot -> previousSnapshots.get(vehicleSnapshot.getVehicle().getName())
.isStranded()
&& !vehicleSnapshot.isStranded()
)
.collect(Collectors.toSet());
}
private boolean isInStrandedState(Vehicle vehicle) {
return isIdleAtNoParkingPosition(vehicle) || isIdleWithTransportOrder(vehicle);
}
private boolean isIdleAtNoParkingPosition(Vehicle vehicle) {
return vehicle.hasState(Vehicle.State.IDLE)
&& vehicle.getCurrentPosition() != null
&& objectService.fetchObject(Point.class, vehicle.getCurrentPosition())
.getType() != Point.Type.PARK_POSITION;
}
private boolean isIdleWithTransportOrder(Vehicle vehicle) {
return vehicle.hasState(Vehicle.State.IDLE)
&& vehicle.getTransportOrder() != null;
}
private boolean relevantPropertiesChanged(
VehicleSnapshot previousSnapshot,
VehicleSnapshot currentSnapshot
) {
return previousSnapshot.getLastState() != currentSnapshot.getLastState()
|| !Objects.equals(previousSnapshot.getLastPosition(), currentSnapshot.getLastPosition())
|| !Objects.equals(
previousSnapshot.getVehicle().getTransportOrder(),
currentSnapshot.getVehicle().getTransportOrder()
);
}
/**
* A snapshot of a vehicle's state with additional information on whether the given vehicle is
* considered stranded and when the last state change (with regard to the "stranded" state)
* occurred.
*/
public static class VehicleSnapshot {
private final Vehicle vehicle;
private long lastRelevantStateChange;
private boolean stranded;
/**
* Creates a new instance.
*
* @param vehicle Vehicle to be stored.
*/
public VehicleSnapshot(Vehicle vehicle) {
this.vehicle = requireNonNull(vehicle, "vehicle");
}
public boolean isStranded() {
return stranded;
}
public void setStranded(boolean stranded) {
this.stranded = stranded;
}
public Vehicle getVehicle() {
return vehicle;
}
public long getLastRelevantStateChange() {
return lastRelevantStateChange;
}
public void setLastRelevantStateChange(long lastRelevantStateChange) {
this.lastRelevantStateChange = lastRelevantStateChange;
}
public TCSObjectReference<Point> getLastPosition() {
return vehicle.getCurrentPosition();
}
public Vehicle.State getLastState() {
return vehicle.getState();
}
@Override
public String toString() {
return "VehicleSnapshot{"
+ "stranded=" + stranded
+ ", lastRelevantStateChange=" + lastRelevantStateChange
+ ", vehicle=" + vehicle
+ '}';
}
}
}

View File

@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.watchdog;
/**
* Provides the current time.
*/
public class TimeProvider {
/**
* Creates a new instance.
*/
public TimeProvider() {
}
/**
* Returns the current time.
*
* @return The current time.
*/
public long getCurrentTime() {
return System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.watchdog;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import org.opentcs.components.kernel.KernelExtension;
/**
* A kernel extension to periodicly monitor the state of the kernel with check tasks.
*/
public class Watchdog
implements
KernelExtension {
/**
* Whether this kernel extension is initialized.
*/
private boolean initialized;
/**
* The task to check for consistency of blocks.
*/
private final BlockConsistencyCheck blockCheck;
/**
* The task to check for stranded vehicles.
*/
private final StrandedVehicleCheck strandedVehicleCheck;
/**
* Creates a new instance.
*
* @param blockCheck The block check task.
* @param strandedVehicleCheck The stranded vehicle check task.
*/
@Inject
public Watchdog(
BlockConsistencyCheck blockCheck,
StrandedVehicleCheck strandedVehicleCheck
) {
this.blockCheck = requireNonNull(blockCheck, "blockCheck");
this.strandedVehicleCheck = requireNonNull(strandedVehicleCheck, "strandedVehicleCheck");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
blockCheck.initialize();
strandedVehicleCheck.initialize();
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
blockCheck.terminate();
strandedVehicleCheck.terminate();
initialized = false;
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.extensions.watchdog;
import org.opentcs.configuration.ConfigurationEntry;
import org.opentcs.configuration.ConfigurationPrefix;
/**
* Configuration for the watchdog extension.
*/
@ConfigurationPrefix(WatchdogConfiguration.PREFIX)
public interface WatchdogConfiguration {
/**
* This configuration's prefix.
*/
String PREFIX = "watchdog";
@ConfigurationEntry(
type = "Integer",
description = "The interval (in milliseconds) in which to check for block consistency.",
changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START,
orderKey = "1_block"
)
int blockConsistencyCheckInterval();
@ConfigurationEntry(
type = "Integer",
description = "The interval (in milliseconds) in which to check for stranded vehicles.",
changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START,
orderKey = "2_stranded_vehicle_0"
)
int strandedVehicleCheckInterval();
@ConfigurationEntry(
type = "Integer",
description = "The duration (in milliseconds) that a vehicle must be in a _stranded_ state "
+ "to actually be considered stranded.",
changesApplied = ConfigurationEntry.ChangesApplied.INSTANTLY,
orderKey = "2_stranded_vehicle_1"
)
int strandedVehicleDurationThreshold();
}

View File

@@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
/**
* The openTCS kernel, its interface for client applications and closely related
* functionality.
*/
package org.opentcs.kernel;

View File

@@ -0,0 +1,178 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkState;
import com.google.inject.assistedinject.Assisted;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import java.util.Objects;
import org.opentcs.components.kernel.services.InternalPeripheralService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.PeripheralInformation;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.peripherals.PeripheralJob;
import org.opentcs.drivers.peripherals.PeripheralAdapterCommand;
import org.opentcs.drivers.peripherals.PeripheralCommAdapter;
import org.opentcs.drivers.peripherals.PeripheralController;
import org.opentcs.drivers.peripherals.PeripheralJobCallback;
import org.opentcs.drivers.peripherals.PeripheralProcessModel;
import org.opentcs.drivers.peripherals.management.PeripheralProcessModelEvent;
import org.opentcs.util.ExplainedBoolean;
import org.opentcs.util.event.EventBus;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Realizes a bidirectional connection between the kernel and a comm adapter controlling a
* peripheral device.
*/
public class DefaultPeripheralController
implements
PeripheralController,
EventHandler {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(DefaultPeripheralController.class);
/**
* The location representing the peripheral device controlled by this controller/the comm adapter.
*/
private final TCSResourceReference<Location> location;
/**
* The comm adapter controling the peripheral device.
*/
private final PeripheralCommAdapter commAdapter;
/**
* The peripheral service to use.
*/
private final InternalPeripheralService peripheralService;
/**
* The event bus we should register with and send events to.
*/
private final EventBus eventBus;
/**
* Indicates whether this controller is initialized.
*/
private boolean initialized;
/**
* Creates a new DefaultPeripheralController.
*
* @param location The location representing the peripheral device.
* @param commAdapter The comm adapter that controls the peripheral device.
* @param peripheralService The peripheral service to be used.
* @param eventBus The event bus to be used.
*/
@Inject
public DefaultPeripheralController(
@Assisted
@Nonnull
TCSResourceReference<Location> location,
@Assisted
@Nonnull
PeripheralCommAdapter commAdapter,
@Nonnull
InternalPeripheralService peripheralService,
@Nonnull
@ApplicationEventBus
EventBus eventBus
) {
this.location = requireNonNull(location, "location");
this.commAdapter = requireNonNull(commAdapter, "commAdapter");
this.peripheralService = requireNonNull(peripheralService, "peripheralService");
this.eventBus = requireNonNull(eventBus, "eventBus");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
eventBus.subscribe(this);
updatePeripheralState(commAdapter.getProcessModel().getState());
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
updatePeripheralState(PeripheralInformation.State.UNKNOWN);
eventBus.unsubscribe(this);
initialized = false;
}
@Override
public void onEvent(Object event) {
if (!(event instanceof PeripheralProcessModelEvent)) {
return;
}
PeripheralProcessModelEvent processModelEvent = (PeripheralProcessModelEvent) event;
if (Objects.equals(
processModelEvent.getAttributeChanged(),
PeripheralProcessModel.Attribute.STATE.name()
)
&& Objects.equals(processModelEvent.getLocation(), location)) {
updatePeripheralState(processModelEvent.getProcessModel().getState());
}
}
@Override
public void process(PeripheralJob job, PeripheralJobCallback callback)
throws IllegalStateException {
requireNonNull(job, "job");
requireNonNull(callback, "callback");
ExplainedBoolean canProcess = canProcess(job);
checkState(
canProcess.getValue(),
"%s: Can't process job: %s",
location.getName(),
canProcess.getReason()
);
LOG.debug("{}: Handing job to comm adapter: {}", location.getName(), job);
commAdapter.process(job, callback);
}
@Override
public void abortJob() {
commAdapter.abortJob();
}
@Override
public ExplainedBoolean canProcess(PeripheralJob job) {
requireNonNull(job, "job");
return commAdapter.canProcess(job);
}
@Override
public void sendCommAdapterCommand(PeripheralAdapterCommand command) {
requireNonNull(command, "command");
commAdapter.execute(command);
}
private void updatePeripheralState(PeripheralInformation.State newState) {
requireNonNull(newState, "newState");
peripheralService.updatePeripheralState(location, newState);
}
}

View File

@@ -0,0 +1,192 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.drivers.peripherals.PeripheralCommAdapter;
import org.opentcs.drivers.peripherals.PeripheralController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Maintains associations of {@link Location}, {@link PeripheralController} and
* {@link PeripheralCommAdapter}.
*/
public class DefaultPeripheralControllerPool
implements
LocalPeripheralControllerPool {
/**
* This class's logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(DefaultPeripheralControllerPool.class);
/**
* The object service to use.
*/
private final TCSObjectService objectService;
/**
* A factory for peripheral controllers.
*/
private final PeripheralControllerFactory controllerFactory;
/**
* The entries of this pool mapped to the corresponding locations.
*/
private final Map<TCSResourceReference<Location>, PoolEntry> poolEntries = new HashMap<>();
/**
* Indicates whether this component is initialized.
*/
private boolean initialized;
/**
* Creates a new DefaultPeripheralControllerPool.
*
* @param objectService The object service to be used.
* @param controllerFactory The controller factory to be used.
*/
@Inject
public DefaultPeripheralControllerPool(
TCSObjectService objectService,
PeripheralControllerFactory controllerFactory
) {
this.objectService = requireNonNull(objectService, "objectService");
this.controllerFactory = requireNonNull(controllerFactory, "controllerFactory");
}
@Override
public void initialize() {
if (isInitialized()) {
LOG.debug("Already initialized, doing nothing.");
return;
}
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
LOG.debug("Not initialized, doing nothing.");
return;
}
// Detach all peripherals.
for (PoolEntry curEntry : poolEntries.values()) {
curEntry.controller.terminate();
}
poolEntries.clear();
initialized = false;
}
@Override
public PeripheralController getPeripheralController(TCSResourceReference<Location> locationRef)
throws IllegalArgumentException {
requireNonNull(locationRef, "locationRef");
checkArgument(
poolEntries.containsKey(locationRef),
"No controller present for %s",
locationRef.getName()
);
return poolEntries.get(locationRef).getController();
}
@Override
public void attachPeripheralController(
TCSResourceReference<Location> locationRef,
PeripheralCommAdapter commAdapter
)
throws IllegalArgumentException {
requireNonNull(locationRef, "locationRef");
requireNonNull(commAdapter, "commAdapter");
if (poolEntries.containsKey(locationRef)) {
LOG.warn("{}: Peripheral controller already attached, doing nothing.", locationRef.getName());
return;
}
Location location = objectService.fetchObject(Location.class, locationRef);
checkArgument(location != null, "No such location: %s", locationRef.getName());
LOG.debug("{}: Attaching controller...", locationRef.getName());
PeripheralController controller = controllerFactory.createPeripheralController(
locationRef,
commAdapter
);
poolEntries.put(locationRef, new PoolEntry(locationRef, controller, commAdapter));
controller.initialize();
}
@Override
public void detachPeripheralController(TCSResourceReference<Location> locationRef) {
requireNonNull(locationRef, "locationRef");
if (!poolEntries.containsKey(locationRef)) {
LOG.debug("{}: No peripheral controller attached, doing nothing.", locationRef.getName());
return;
}
LOG.debug("{}: Detaching controller...", locationRef.getName());
poolEntries.remove(locationRef).getController().terminate();
}
/**
* An entry in this controller pool.
*/
private static class PoolEntry {
/**
* The location.
*/
private final TCSResourceReference<Location> location;
/**
* The peripheral controller associated with the location.
*/
private final PeripheralController controller;
/**
* The comm adapter associated with the location.
*/
private final PeripheralCommAdapter commAdapter;
/**
* Creates a new pool entry.
*
* @param location The location.
* @param controller The peripheral controller associated with the location.
* @param cmmmAdapter The comm adapter associated with the location.
*/
private PoolEntry(
TCSResourceReference<Location> location,
PeripheralController controller,
PeripheralCommAdapter cmmmAdapter
) {
this.location = requireNonNull(location, "location");
this.controller = requireNonNull(controller, "controller");
this.commAdapter = requireNonNull(cmmmAdapter, "cmmmAdapter");
}
public TCSResourceReference<Location> getLocation() {
return location;
}
public PeripheralController getController() {
return controller;
}
public PeripheralCommAdapter getCommAdapter() {
return commAdapter;
}
}
}

View File

@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import org.opentcs.components.Lifecycle;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.drivers.peripherals.PeripheralCommAdapter;
import org.opentcs.drivers.peripherals.PeripheralControllerPool;
/**
* Manages the attachment of peripheral controllers to locations and peripheral comm adapters.
*/
public interface LocalPeripheralControllerPool
extends
PeripheralControllerPool,
Lifecycle {
/**
* Associates a peripheral controller with a location and a comm adapter.
*
* @param location The reference to the location.
* @param commAdapter The comm adapter that is going to control the peripheral deivce.
* @throws IllegalArgumentException If the referenced location does not exist.
*/
void attachPeripheralController(
TCSResourceReference<Location> location,
PeripheralCommAdapter commAdapter
)
throws IllegalArgumentException;
/**
* Disassociates a peripheral controller and a comm adapter from a location.
*
* @param location The reference to the location.
*/
void detachPeripheralController(TCSResourceReference<Location> location);
}

View File

@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import jakarta.annotation.Nonnull;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.peripherals.PeripheralJob;
import org.opentcs.drivers.peripherals.PeripheralAdapterCommand;
import org.opentcs.drivers.peripherals.PeripheralCommAdapter;
import org.opentcs.drivers.peripherals.PeripheralJobCallback;
import org.opentcs.drivers.peripherals.PeripheralProcessModel;
import org.opentcs.util.ExplainedBoolean;
/**
* A {@link PeripheralCommAdapter} implementation that is doing nothing.
*/
public class NullPeripheralCommAdapter
implements
PeripheralCommAdapter {
/**
* The process model.
*/
private final PeripheralProcessModel processModel;
/**
* Creates a new instance.
*
* @param location The reference to the location this adapter is attached to.
*/
public NullPeripheralCommAdapter(
@Nonnull
TCSResourceReference<Location> location
) {
this.processModel = new PeripheralProcessModel(location);
}
@Override
public void initialize() {
}
@Override
public boolean isInitialized() {
return true;
}
@Override
public void terminate() {
}
@Override
public void enable() {
}
@Override
public void disable() {
}
@Override
public boolean isEnabled() {
return false;
}
@Override
public PeripheralProcessModel getProcessModel() {
return processModel;
}
@Override
public ExplainedBoolean canProcess(PeripheralJob job) {
return new ExplainedBoolean(false, "Can't process any jobs.");
}
@Override
public void process(PeripheralJob job, PeripheralJobCallback callback) {
}
@Override
public void abortJob() {
}
@Override
public void execute(PeripheralAdapterCommand command) {
}
}

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import org.opentcs.common.peripherals.NullPeripheralCommAdapterDescription;
import org.opentcs.data.model.Location;
import org.opentcs.drivers.peripherals.PeripheralCommAdapter;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory;
/**
* A factory for {@link NullPeripheralCommAdapter}s.
*/
public class NullPeripheralCommAdapterFactory
implements
PeripheralCommAdapterFactory {
/**
* Creates a new NullPeripheralCommAdapterFactory.
*/
public NullPeripheralCommAdapterFactory() {
}
@Override
public PeripheralCommAdapterDescription getDescription() {
return new NullPeripheralCommAdapterDescription();
}
@Override
public boolean providesAdapterFor(Location location) {
return true;
}
@Override
public PeripheralCommAdapter getAdapterFor(Location location) {
return new NullPeripheralCommAdapter(location.getReference());
}
@Override
public void initialize() {
}
@Override
public boolean isInitialized() {
return true;
}
@Override
public void terminate() {
}
}

View File

@@ -0,0 +1,282 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import java.util.List;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.InternalPeripheralService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.drivers.peripherals.PeripheralCommAdapter;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory;
import org.opentcs.drivers.peripherals.PeripheralProcessModel;
import org.opentcs.drivers.peripherals.management.PeripheralAttachmentEvent;
import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation;
import org.opentcs.drivers.peripherals.management.PeripheralProcessModelEvent;
import org.opentcs.kernel.KernelApplicationConfiguration;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages attachment and detachment of peripheral communication adapters to location.
*/
public class PeripheralAttachmentManager
implements
Lifecycle {
/**
* This class's logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(PeripheralAttachmentManager.class);
/**
* This class's configuration.
*/
private final KernelApplicationConfiguration configuration;
/**
* The peripheral service.
*/
private final InternalPeripheralService peripheralService;
/**
* The peripheral controller pool.
*/
private final LocalPeripheralControllerPool controllerPool;
/**
* The peripheral comm adapter registry.
*/
private final PeripheralCommAdapterRegistry commAdapterRegistry;
/**
* The pool of peripheral entries.
*/
private final PeripheralEntryPool peripheralEntryPool;
/**
* The handler to send events to.
*/
private final EventHandler eventHandler;
/**
* Whether the attachment manager is initialized or not.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param peripheralService The peripheral service.
* @param controllerPool The peripheral controller pool.
* @param commAdapterRegistry The peripheral comm adapter registry.
* @param peripheralEntryPool The pool of peripheral entries.
* @param eventHandler The handler to send events to.
* @param configuration This class's configuration.
*/
@Inject
public PeripheralAttachmentManager(
@Nonnull
InternalPeripheralService peripheralService,
@Nonnull
LocalPeripheralControllerPool controllerPool,
@Nonnull
PeripheralCommAdapterRegistry commAdapterRegistry,
@Nonnull
PeripheralEntryPool peripheralEntryPool,
@Nonnull
@ApplicationEventBus
EventHandler eventHandler,
@Nonnull
KernelApplicationConfiguration configuration
) {
this.peripheralService = requireNonNull(peripheralService, "peripheralService");
this.controllerPool = requireNonNull(controllerPool, "controllerPool");
this.commAdapterRegistry = requireNonNull(commAdapterRegistry, "commAdapterRegistry");
this.peripheralEntryPool = requireNonNull(peripheralEntryPool, "peripheralEntryPool");
this.eventHandler = requireNonNull(eventHandler, "eventHandler");
this.configuration = requireNonNull(configuration, "configuration");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
commAdapterRegistry.initialize();
peripheralEntryPool.initialize();
autoAttachAllAdapters();
LOG.debug("Locations attached: {}", peripheralEntryPool.getEntries());
if (configuration.autoEnablePeripheralDriversOnStartup()) {
autoEnableAllAdapters();
}
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
LOG.debug("Not initialized.");
return;
}
// Disable and terminate all attached drivers to clean up.
disableAndTerminateAllAdapters();
peripheralEntryPool.terminate();
commAdapterRegistry.terminate();
initialized = false;
}
/**
* Attaches a peripheral comm adapter to a location.
*
* @param location The location to attach to.
* @param description The description of the comm adapter to attach.
*/
public void attachAdapterToLocation(
@Nonnull
TCSResourceReference<Location> location,
@Nonnull
PeripheralCommAdapterDescription description
) {
requireNonNull(location, "location");
requireNonNull(description, "description");
attachAdapterToLocation(
peripheralEntryPool.getEntryFor(location),
commAdapterRegistry.findFactoryFor(description)
);
}
/**
* Returns the attachment information for a location.
*
* @param location The location to get attachment information about.
* @return The attachment information for a location.
*/
@Nonnull
public PeripheralAttachmentInformation getAttachmentInformation(
@Nonnull
TCSResourceReference<Location> location
) {
requireNonNull(location, "location");
PeripheralEntry entry = peripheralEntryPool.getEntryFor(location);
return new PeripheralAttachmentInformation(
entry.getLocation(),
entry.getAvailableCommAdapters(),
entry.getCommAdapterFactory().getDescription()
);
}
private void attachAdapterToLocation(
PeripheralEntry entry,
PeripheralCommAdapterFactory factory
) {
requireNonNull(entry, "entry");
requireNonNull(factory, "factory");
LOG.info(
"Attaching peripheral comm adapter: '{}' -- '{}'...",
entry.getLocation().getName(),
factory.getClass().getName()
);
Location location = peripheralService.fetchObject(Location.class, entry.getLocation());
PeripheralCommAdapter commAdapter = factory.getAdapterFor(location);
if (commAdapter == null) {
LOG.warn(
"Factory {} did not provide adapter for location {}, ignoring.",
factory,
entry.getLocation().getName()
);
return;
}
// Perform a cleanup for the old adapter.
disableAndTerminateAdapter(entry);
controllerPool.detachPeripheralController(entry.getLocation());
commAdapter.initialize();
controllerPool.attachPeripheralController(entry.getLocation(), commAdapter);
entry.setCommAdapterFactory(factory);
entry.setCommAdapter(commAdapter);
// Publish events about the new attached adapter.
eventHandler.onEvent(
new PeripheralAttachmentEvent(
entry.getLocation(),
new PeripheralAttachmentInformation(
entry.getLocation(),
entry.getAvailableCommAdapters(),
entry.getCommAdapterFactory().getDescription()
)
)
);
eventHandler.onEvent(
new PeripheralProcessModelEvent(
entry.getLocation(),
PeripheralProcessModel.Attribute.LOCATION.name(),
entry.getProcessModel()
)
);
}
private void autoAttachAdapterToLocation(PeripheralEntry peripheralEntry) {
// Do not auto-attach if there is already a (real) comm adapter attached to the location.
if (!(peripheralEntry.getCommAdapter() instanceof NullPeripheralCommAdapter)) {
return;
}
Location location = peripheralService.fetchObject(
Location.class,
peripheralEntry.getLocation()
);
List<PeripheralCommAdapterFactory> factories = commAdapterRegistry.findFactoriesFor(location);
if (!factories.isEmpty()) {
LOG.debug(
"Attaching {} to first available adapter: {}.",
peripheralEntry.getLocation().getName(),
factories.get(0).getDescription().getDescription()
);
attachAdapterToLocation(peripheralEntry, factories.get(0));
}
}
private void autoAttachAllAdapters() {
peripheralEntryPool.getEntries().forEach((location, entry) -> {
autoAttachAdapterToLocation(entry);
});
}
private void disableAndTerminateAdapter(PeripheralEntry peripheralEntry) {
peripheralEntry.getCommAdapter().disable();
peripheralEntry.getCommAdapter().terminate();
}
private void autoEnableAllAdapters() {
peripheralEntryPool.getEntries().values().stream()
.map(entry -> entry.getCommAdapter())
.filter(adapter -> !adapter.isEnabled())
.forEach(adapter -> adapter.enable());
}
private void disableAndTerminateAllAdapters() {
LOG.debug("Detaching peripheral communication adapters...");
peripheralEntryPool.getEntries().forEach((location, entry) -> {
disableAndTerminateAdapter(entry);
});
LOG.debug("Detached peripheral communication adapters");
}
}

View File

@@ -0,0 +1,135 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.opentcs.components.Lifecycle;
import org.opentcs.data.model.Location;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A registry for all peripheral communication adapters in the system.
*/
public class PeripheralCommAdapterRegistry
implements
Lifecycle {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(PeripheralCommAdapterRegistry.class);
/**
* The registered factories.
*/
private final Map<PeripheralCommAdapterDescription, PeripheralCommAdapterFactory> factories
= new HashMap<>();
/**
* Indicates whether this component is initialized or not.
*/
private boolean initialized;
/**
* Creates a new registry.
*
* @param factories The peripheral comm adapter factories.
*/
@Inject
public PeripheralCommAdapterRegistry(Set<PeripheralCommAdapterFactory> factories) {
requireNonNull(factories, "factories");
for (PeripheralCommAdapterFactory factory : factories) {
LOG.info(
"Setting up peripheral communication adapter factory: {}",
factory.getClass().getName()
);
this.factories.put(factory.getDescription(), factory);
}
}
@Override
public void initialize() {
if (initialized) {
return;
}
for (PeripheralCommAdapterFactory factory : factories.values()) {
factory.initialize();
}
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!initialized) {
return;
}
for (PeripheralCommAdapterFactory factory : factories.values()) {
factory.terminate();
}
initialized = false;
}
/**
* Returns all registered factories that can provide peripheral communication adapters.
*
* @return All registered factories that can provide peripheral communication adapters.
*/
public List<PeripheralCommAdapterFactory> getFactories() {
return new ArrayList<>(factories.values());
}
/**
* Returns the factory for the given description.
*
* @param description The description to get the factory for.
* @return The factory for the given description.
*/
@Nonnull
public PeripheralCommAdapterFactory findFactoryFor(
@Nonnull
PeripheralCommAdapterDescription description
) {
requireNonNull(description, "description");
checkArgument(
factories.get(description) != null,
"No factory for description %s",
description
);
return factories.get(description);
}
/**
* Returns a set of factories that can provide communication adapters for the given location.
*
* @param location The location to find communication adapters/factories for.
* @return A set of factories that can provide communication adapters for the given location.
*/
public List<PeripheralCommAdapterFactory> findFactoriesFor(Location location) {
requireNonNull(location, "location");
return factories.values().stream()
.filter(factory -> factory.providesAdapterFor(location))
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.drivers.peripherals.PeripheralCommAdapter;
import org.opentcs.drivers.peripherals.PeripheralController;
/**
* A factory for {@link PeripheralController} instances.
*/
public interface PeripheralControllerFactory {
/**
* Creates a new peripheral controller for the given location and communication adapter.
*
* @param location The location.
* @param commAdapter The communication adapter.
* @return A new peripheral controller.
*/
DefaultPeripheralController createPeripheralController(
TCSResourceReference<Location> location,
PeripheralCommAdapter commAdapter
);
}

View File

@@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import java.util.List;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.drivers.peripherals.PeripheralCommAdapter;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterFactory;
import org.opentcs.drivers.peripherals.PeripheralProcessModel;
/**
* An entry for a peripheral device represented by a {@link Location}.
*/
public class PeripheralEntry {
/**
* The available comm adapters for this entry.
*/
private final List<PeripheralCommAdapterDescription> availableCommAdapters;
/**
* The peripheral comm adapter factory that created this entry's comm adapter instance.
*/
private PeripheralCommAdapterFactory commAdapterFactory = new NullPeripheralCommAdapterFactory();
/**
* The comm adapter instance for this entry.
*/
private PeripheralCommAdapter commAdapter;
/**
* Creates a new instance.
*
* @param location The location representing the peripheral device.
* @param availableCommAdapters The available comm adapters for this entry.
*/
public PeripheralEntry(
@Nonnull
Location location,
@Nonnull
List<PeripheralCommAdapterDescription> availableCommAdapters
) {
requireNonNull(location, "location");
this.availableCommAdapters = requireNonNull(availableCommAdapters, "availableCommAdapters");
this.commAdapter = commAdapterFactory.getAdapterFor(location);
}
@Nonnull
public PeripheralProcessModel getProcessModel() {
return commAdapter.getProcessModel();
}
@Nonnull
public TCSResourceReference<Location> getLocation() {
return getProcessModel().getLocation();
}
@Nonnull
public List<PeripheralCommAdapterDescription> getAvailableCommAdapters() {
return availableCommAdapters;
}
@Nonnull
public PeripheralCommAdapterFactory getCommAdapterFactory() {
return commAdapterFactory;
}
public void setCommAdapterFactory(
@Nonnull
PeripheralCommAdapterFactory commAdapterFactory
) {
this.commAdapterFactory = requireNonNull(commAdapterFactory, "commAdapterFactory");
}
@Nonnull
public PeripheralCommAdapter getCommAdapter() {
return commAdapter;
}
public void setCommAdapter(
@Nonnull
PeripheralCommAdapter commAdapter
) {
this.commAdapter = requireNonNull(commAdapter, "commAdapter");
}
@Override
public String toString() {
return "PeripheralEntry{"
+ "availableCommAdapters=" + availableCommAdapters + ", "
+ "commAdapterFactory=" + commAdapterFactory + ", "
+ "commAdapter=" + commAdapter + '}';
}
}

View File

@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.peripherals;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides a pool of {@link PeripheralEntry}s with an entry for every {@link Location} object in
* the kernel.
*/
public class PeripheralEntryPool
implements
Lifecycle {
/**
* This class's logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(PeripheralEntryPool.class);
/**
* The object service.
*/
private final TCSObjectService objectService;
/**
* The peripheral comm adapter registry.
*/
private final PeripheralCommAdapterRegistry commAdapterRegistry;
/**
* The entries of this pool.
*/
private final Map<TCSResourceReference<Location>, PeripheralEntry> entries = new HashMap<>();
/**
* Whether the pool is initialized or not.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param objectService The object service.
* @param commAdapterRegistry The peripheral comm adapter registry.
*/
@Inject
public PeripheralEntryPool(
@Nonnull
TCSObjectService objectService,
@Nonnull
PeripheralCommAdapterRegistry commAdapterRegistry
) {
this.objectService = requireNonNull(objectService, "objectService");
this.commAdapterRegistry = requireNonNull(commAdapterRegistry, "commAdapterRegistry");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
for (Location location : objectService.fetchObjects(Location.class)) {
entries.put(
location.getReference(),
new PeripheralEntry(
location,
commAdapterRegistry.findFactoriesFor(location).stream()
.map(factory -> factory.getDescription())
.collect(Collectors.toList())
)
);
}
LOG.debug("Initialized peripheral entry pool: {}", entries);
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
entries.clear();
initialized = false;
}
@Nonnull
public Map<TCSResourceReference<Location>, PeripheralEntry> getEntries() {
return entries;
}
/**
* Returns the {@link PeripheralEntry} for the given location.
*
* @param location The reference to the location.
* @return The entry for the given location.
* @throws IllegalArgumentException If no entry is present for the given location.
*/
@Nonnull
public PeripheralEntry getEntryFor(
@Nonnull
TCSResourceReference<Location> location
)
throws IllegalArgumentException {
requireNonNull(location, "location");
checkArgument(
entries.containsKey(location),
"No peripheral entry present for %s",
location.getName()
);
return entries.get(location);
}
}

View File

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.persistence;
import org.opentcs.access.to.model.PlantModelCreationTO;
/**
* Provides methods to persist and load models.
* Only a single model is persisted at a time.
*/
public interface ModelPersister {
/**
* Find out if there is a persisted model at the moment.
*
* @return True if a model is saved.
*/
boolean hasSavedModel();
/**
* Persists a model according to the actual implementation of this method.
*
* @param model The model to be persisted.
* @throws IllegalStateException If persisting the model is not possible for some reason.
*/
void saveModel(PlantModelCreationTO model)
throws IllegalStateException;
/**
* Reads the model and returns it as a <Code>PlantModelCreationTO</Code>.
*
* @return The <Code>PlantModelCreationTO</Code> that contains the data that was read.
* @throws IllegalStateException If reading the model is not possible for some reason.
*/
PlantModelCreationTO readModel()
throws IllegalStateException;
}

View File

@@ -0,0 +1,186 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.persistence;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkState;
import jakarta.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import org.opentcs.access.to.model.PlantModelCreationTO;
import org.opentcs.customizations.ApplicationHome;
import org.opentcs.util.persistence.ModelParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link ModelPersister} implementation using an XML file.
*/
public class XMLFileModelPersister
implements
ModelPersister {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(XMLFileModelPersister.class);
/**
* The name of the model file in the model directory.
*/
private static final String MODEL_FILE_NAME = "model.xml";
/**
* The directory path for the persisted model.
*/
private final File dataDirectory;
/**
* The model file.
*/
private final File modelFile;
/**
* Reads and writes models into xml files.
*/
private final ModelParser modelParser;
/**
* Creates a new XMLFileModelPersister.
*
* @param directory The application's home directory.
* @param modelParser Reads and writes into the xml file.
*/
@Inject
public XMLFileModelPersister(
@ApplicationHome
File directory,
ModelParser modelParser
) {
this.modelParser = requireNonNull(modelParser, "modelParser");
this.dataDirectory = new File(requireNonNull(directory, "directory"), "data");
this.modelFile = new File(dataDirectory, MODEL_FILE_NAME);
}
@Override
public void saveModel(PlantModelCreationTO model)
throws IllegalStateException {
requireNonNull(model, "model");
LOG.debug("Saving model '{}'.", model.getName());
// Check if writing the model is possible.
checkState(
dataDirectory.isDirectory() || dataDirectory.mkdirs(),
"%s is not an existing directory and could not be created, either.",
dataDirectory.getPath()
);
checkState(
!modelFile.exists() || modelFile.isFile(),
"%s exists, but is not a regular file",
modelFile.getPath()
);
try {
if (modelFile.exists()) {
createBackup();
}
modelParser.writeModel(model, modelFile);
}
catch (IOException exc) {
throw new IllegalStateException("Exception saving model", exc);
}
}
@Override
public PlantModelCreationTO readModel()
throws IllegalStateException {
// Return empty model if there is no saved model
if (!hasSavedModel()) {
return new PlantModelCreationTO("empty model");
}
// Read the model from the file.
return readXMLModel(modelFile);
}
@Override
public boolean hasSavedModel() {
return modelFileExists();
}
/**
* Creates a backup of the currently saved model file by copying it to the
* "backups" subdirectory.
*
* Assumes that the model file exists.
*
* @throws IOException If the backup directory is not accessible or copying
* the file fails.
*/
private void createBackup()
throws IOException {
// Generate backup file name
Calendar cal = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss-SSS");
String time = sdf.format(cal.getTime());
String modelBackupName = MODEL_FILE_NAME + "_backup_" + time;
// Make sure backup directory exists
File modelBackupDirectory = new File(dataDirectory, "backups");
if (modelBackupDirectory.exists()) {
if (!modelBackupDirectory.isDirectory()) {
throw new IOException(
modelBackupDirectory.getPath() + " exists, but is not a directory"
);
}
}
else {
if (!modelBackupDirectory.mkdir()) {
throw new IOException(
"Could not create model directory " + modelBackupDirectory.getPath()
);
}
}
// Backup the model file
Files.copy(
modelFile.toPath(),
new File(modelBackupDirectory, modelBackupName).toPath()
);
}
/**
* Test if the data directory with a model file exist. If not, throw an
* exception.
*
* @throws IOException If check failed.
*/
private boolean modelFileExists() {
if (!modelFile.exists()) {
return false;
}
if (!modelFile.isFile()) {
return false;
}
return true;
}
/**
* Reads a model from a given InputStream.
*
* @param modelFile The file containing the model.
* @param model The model to be built.
* @throws IOException If an exception occured while loading
*/
private PlantModelCreationTO readXMLModel(File modelFile)
throws IllegalStateException {
try {
return modelParser.readModel(modelFile);
}
catch (IOException exc) {
LOG.error("Exception parsing input", exc);
throw new IllegalStateException("Exception parsing input", exc);
}
}
}

View File

@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
/**
* Classes for persisting and materializing openTCS data.
*/
package org.opentcs.kernel.persistence;

View File

@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.util.Set;
import java.util.function.Predicate;
import org.opentcs.access.CredentialsException;
import org.opentcs.access.KernelRuntimeException;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.data.ObjectHistory;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObject;
import org.opentcs.data.TCSObjectReference;
/**
* Delegate method calls to the {@link TCSObjectService} implementation.
*/
public abstract class AbstractTCSObjectService
implements
TCSObjectService {
/**
* The tcs object service to delegate method calls to.
*/
private final TCSObjectService objectService;
/**
* Creates a new instance.
*
* @param objectService The service to delegate method calls to.
*/
public AbstractTCSObjectService(TCSObjectService objectService) {
this.objectService = requireNonNull(objectService, "objectService");
}
@Override
public <T extends TCSObject<T>> T fetchObject(Class<T> clazz, TCSObjectReference<T> ref)
throws CredentialsException {
requireNonNull(clazz, "clazz");
requireNonNull(ref, "ref");
return getObjectService().fetchObject(clazz, ref);
}
@Override
public <T extends TCSObject<T>> T fetchObject(Class<T> clazz, String name)
throws CredentialsException {
requireNonNull(clazz, "clazz");
return getObjectService().fetchObject(clazz, name);
}
@Override
public <T extends TCSObject<T>> Set<T> fetchObjects(Class<T> clazz)
throws CredentialsException {
requireNonNull(clazz, "clazz");
return getObjectService().fetchObjects(clazz);
}
@Override
public <T extends TCSObject<T>> Set<T> fetchObjects(
@Nonnull
Class<T> clazz,
@Nonnull
Predicate<? super T> predicate
)
throws CredentialsException {
requireNonNull(clazz, "clazz");
requireNonNull(predicate, "predicate");
return getObjectService().fetchObjects(clazz, predicate);
}
@Override
public void updateObjectProperty(
TCSObjectReference<?> ref,
String key,
@Nullable
String value
)
throws ObjectUnknownException,
CredentialsException {
requireNonNull(ref, "ref");
requireNonNull(key, "key");
getObjectService().updateObjectProperty(ref, key, value);
}
@Override
public void appendObjectHistoryEntry(TCSObjectReference<?> ref, ObjectHistory.Entry entry)
throws ObjectUnknownException,
KernelRuntimeException {
requireNonNull(ref, "ref");
requireNonNull(entry, "entry");
getObjectService().appendObjectHistoryEntry(ref, entry);
}
/**
* Retruns the {@link TCSObjectService} implementation being used.
*
* @return The {@link TCSObjectService} implementation being used.
*/
public TCSObjectService getObjectService() {
return objectService;
}
}

View File

@@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import org.opentcs.components.kernel.Dispatcher;
import org.opentcs.components.kernel.dipatching.TransportOrderAssignmentException;
import org.opentcs.components.kernel.services.DispatcherService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.order.ReroutingType;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.kernel.workingset.TCSObjectRepository;
/**
* This class is the standard implementation of the {@link DispatcherService} interface.
*/
public class StandardDispatcherService
implements
DispatcherService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The container of all course model and transport order objects.
*/
private final TCSObjectRepository objectRepo;
/**
* The dispatcher.
*/
private final Dispatcher dispatcher;
/**
* Creates a new instance.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param objectRepo The object repo to be used.
* @param dispatcher The dispatcher.
*/
@Inject
public StandardDispatcherService(
@GlobalSyncObject
Object globalSyncObject,
TCSObjectRepository objectRepo,
Dispatcher dispatcher
) {
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.objectRepo = requireNonNull(objectRepo, "objectRepo");
this.dispatcher = requireNonNull(dispatcher, "dispatcher");
}
@Override
public void dispatch() {
synchronized (globalSyncObject) {
dispatcher.dispatch();
}
}
@Override
public void withdrawByVehicle(TCSObjectReference<Vehicle> ref, boolean immediateAbort)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
dispatcher.withdrawOrder(objectRepo.getObject(Vehicle.class, ref), immediateAbort);
}
}
@Override
public void withdrawByTransportOrder(
TCSObjectReference<TransportOrder> ref,
boolean immediateAbort
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
dispatcher.withdrawOrder(
objectRepo.getObject(TransportOrder.class, ref),
immediateAbort
);
}
}
@Override
public void reroute(TCSObjectReference<Vehicle> ref, ReroutingType reroutingType)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(reroutingType, "reroutingType");
synchronized (globalSyncObject) {
dispatcher.reroute(objectRepo.getObject(Vehicle.class, ref), reroutingType);
}
}
@Override
public void rerouteAll(ReroutingType reroutingType) {
synchronized (globalSyncObject) {
dispatcher.rerouteAll(reroutingType);
}
}
@Override
public void assignNow(TCSObjectReference<TransportOrder> ref)
throws ObjectUnknownException,
TransportOrderAssignmentException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
dispatcher.assignNow(objectRepo.getObject(TransportOrder.class, ref));
}
}
}

View File

@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.List;
import java.util.function.Predicate;
import org.opentcs.components.kernel.services.NotificationService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.notification.UserNotification;
import org.opentcs.kernel.workingset.NotificationBuffer;
/**
* This class is the standard implementation of the {@link NotificationService} interface.
*/
public class StandardNotificationService
implements
NotificationService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The buffer for all messages published.
*/
private final NotificationBuffer notificationBuffer;
/**
* Creates a new instance.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param notificationBuffer The notification buffer to be used.
*/
@Inject
public StandardNotificationService(
@GlobalSyncObject
Object globalSyncObject,
NotificationBuffer notificationBuffer
) {
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.notificationBuffer = requireNonNull(notificationBuffer, "notificationBuffer");
}
@Override
public List<UserNotification> fetchUserNotifications(Predicate<UserNotification> predicate) {
synchronized (globalSyncObject) {
return notificationBuffer.getNotifications(predicate);
}
}
@Override
public void publishUserNotification(UserNotification notification) {
requireNonNull(notification, "notification");
synchronized (globalSyncObject) {
notificationBuffer.addNotification(notification);
}
}
}

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import org.opentcs.access.KernelRuntimeException;
import org.opentcs.components.kernel.PeripheralJobDispatcher;
import org.opentcs.components.kernel.services.PeripheralDispatcherService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.peripherals.PeripheralJob;
import org.opentcs.kernel.workingset.TCSObjectRepository;
/**
* This class is the standard implementation of the {@link PeripheralDispatcherService} interface.
*/
public class StandardPeripheralDispatcherService
implements
PeripheralDispatcherService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The container of all course model and transport order objects.
*/
private final TCSObjectRepository objectRepo;
/**
* The peripheral job dispatcher.
*/
private final PeripheralJobDispatcher dispatcher;
/**
* Creates a new instance.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param objectRepo The object repo to be used.
* @param dispatcher The peripheral job dispatcher.
*/
@Inject
public StandardPeripheralDispatcherService(
@GlobalSyncObject
Object globalSyncObject,
TCSObjectRepository objectRepo,
PeripheralJobDispatcher dispatcher
) {
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.objectRepo = requireNonNull(objectRepo, "objectRepo");
this.dispatcher = requireNonNull(dispatcher, "dispatcher");
}
@Override
public void dispatch() {
synchronized (globalSyncObject) {
dispatcher.dispatch();
}
}
@Override
public void withdrawByLocation(TCSResourceReference<Location> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
dispatcher.withdrawJob(objectRepo.getObject(Location.class, ref));
}
}
@Override
public void withdrawByPeripheralJob(TCSObjectReference<PeripheralJob> ref)
throws ObjectUnknownException,
KernelRuntimeException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
dispatcher.withdrawJob(objectRepo.getObject(PeripheralJob.class, ref));
}
}
}

View File

@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import org.opentcs.access.KernelRuntimeException;
import org.opentcs.access.to.peripherals.PeripheralJobCreationTO;
import org.opentcs.components.kernel.services.InternalPeripheralJobService;
import org.opentcs.components.kernel.services.PeripheralJobService;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectExistsException;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.peripherals.PeripheralJob;
import org.opentcs.kernel.workingset.PeripheralJobPoolManager;
/**
* This class is the standard implementation of the {@link PeripheralJobService} interface.
*/
public class StandardPeripheralJobService
extends
AbstractTCSObjectService
implements
InternalPeripheralJobService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The job pool manager.
*/
private final PeripheralJobPoolManager jobPoolManager;
/**
* Creates a new instance.
*
* @param objectService The tcs obejct service.
* @param globalSyncObject The kernel threads' global synchronization object.
* @param jobPoolManager The job pool manager to be used.
*/
@Inject
public StandardPeripheralJobService(
TCSObjectService objectService,
@GlobalSyncObject
Object globalSyncObject,
PeripheralJobPoolManager jobPoolManager
) {
super(objectService);
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.jobPoolManager = requireNonNull(jobPoolManager, "jobPoolManager");
}
@Override
public void updatePeripheralJobState(
TCSObjectReference<PeripheralJob> ref,
PeripheralJob.State state
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(state, "state");
synchronized (globalSyncObject) {
jobPoolManager.setPeripheralJobState(ref, state);
}
}
@Override
public PeripheralJob createPeripheralJob(PeripheralJobCreationTO to)
throws ObjectUnknownException,
ObjectExistsException,
KernelRuntimeException {
requireNonNull(to, "to");
synchronized (globalSyncObject) {
return jobPoolManager.createPeripheralJob(to);
}
}
}

View File

@@ -0,0 +1,204 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import org.opentcs.components.kernel.services.InternalPeripheralService;
import org.opentcs.components.kernel.services.PeripheralService;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.PeripheralInformation;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.peripherals.PeripheralJob;
import org.opentcs.drivers.peripherals.PeripheralAdapterCommand;
import org.opentcs.drivers.peripherals.PeripheralCommAdapterDescription;
import org.opentcs.drivers.peripherals.PeripheralProcessModel;
import org.opentcs.drivers.peripherals.management.PeripheralAttachmentInformation;
import org.opentcs.kernel.peripherals.PeripheralAttachmentManager;
import org.opentcs.kernel.peripherals.PeripheralEntry;
import org.opentcs.kernel.peripherals.PeripheralEntryPool;
import org.opentcs.kernel.workingset.PlantModelManager;
/**
* This class is the standard implementation of the {@link PeripheralService} interface.
*/
public class StandardPeripheralService
extends
AbstractTCSObjectService
implements
InternalPeripheralService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The attachment manager.
*/
private final PeripheralAttachmentManager attachmentManager;
/**
* The pool of peripheral entries.
*/
private final PeripheralEntryPool peripheralEntryPool;
/**
* The plant model manager.
*/
private final PlantModelManager plantModelManager;
/**
* Creates a new instance.
*
* @param objectService The tcs object service.
* @param globalSyncObject The kernel threads' global synchronization object.
* @param attachmentManager The attachment manager.
* @param peripheralEntryPool The pool of peripheral entries.
* @param plantModelManager The plant model manager to be used.
*/
@Inject
public StandardPeripheralService(
TCSObjectService objectService,
@GlobalSyncObject
Object globalSyncObject,
PeripheralAttachmentManager attachmentManager,
PeripheralEntryPool peripheralEntryPool,
PlantModelManager plantModelManager
) {
super(objectService);
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.attachmentManager = requireNonNull(attachmentManager, "attachmentManager");
this.peripheralEntryPool = requireNonNull(peripheralEntryPool, "peripheralEntryPool");
this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager");
}
@Override
public void attachCommAdapter(
TCSResourceReference<Location> ref,
PeripheralCommAdapterDescription description
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(description, "description");
synchronized (globalSyncObject) {
attachmentManager.attachAdapterToLocation(ref, description);
}
}
@Override
public void disableCommAdapter(TCSResourceReference<Location> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
peripheralEntryPool.getEntryFor(ref).getCommAdapter().disable();
}
}
@Override
public void enableCommAdapter(TCSResourceReference<Location> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
peripheralEntryPool.getEntryFor(ref).getCommAdapter().enable();
}
}
@Override
public PeripheralAttachmentInformation fetchAttachmentInformation(
TCSResourceReference<Location> ref
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
return attachmentManager.getAttachmentInformation(ref);
}
}
@Override
public PeripheralProcessModel fetchProcessModel(TCSResourceReference<Location> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
return peripheralEntryPool.getEntryFor(ref).getCommAdapter().getProcessModel();
}
}
@Override
public void sendCommAdapterCommand(
TCSResourceReference<Location> ref,
PeripheralAdapterCommand command
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(command, "command");
synchronized (globalSyncObject) {
PeripheralEntry entry = peripheralEntryPool.getEntryFor(ref);
synchronized (entry.getCommAdapter()) {
entry.getCommAdapter().execute(command);
}
}
}
@Override
public void updatePeripheralProcState(
TCSResourceReference<Location> ref,
PeripheralInformation.ProcState state
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(state, "state");
synchronized (globalSyncObject) {
plantModelManager.setLocationProcState(ref, state);
}
}
@Override
public void updatePeripheralReservationToken(
TCSResourceReference<Location> ref,
String reservationToken
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
plantModelManager.setLocationReservationToken(ref, reservationToken);
}
}
@Override
public void updatePeripheralState(
TCSResourceReference<Location> ref,
PeripheralInformation.State state
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(state, "state");
synchronized (globalSyncObject) {
plantModelManager.setLocationState(ref, state);
}
}
@Override
public void updatePeripheralJob(
TCSResourceReference<Location> ref,
TCSObjectReference<PeripheralJob> peripheralJob
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
plantModelManager.setLocationPeripheralJob(ref, peripheralJob);
}
}
}

View File

@@ -0,0 +1,264 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.Map;
import java.util.Set;
import org.opentcs.access.Kernel;
import org.opentcs.access.KernelRuntimeException;
import org.opentcs.access.LocalKernel;
import org.opentcs.access.ModelTransitionEvent;
import org.opentcs.access.to.model.PlantModelCreationTO;
import org.opentcs.components.kernel.services.InternalPlantModelService;
import org.opentcs.components.kernel.services.NotificationService;
import org.opentcs.components.kernel.services.PlantModelService;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectExistsException;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Block;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.LocationType;
import org.opentcs.data.model.Path;
import org.opentcs.data.model.PlantModel;
import org.opentcs.data.model.Point;
import org.opentcs.data.model.TCSResource;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.model.visualization.VisualLayout;
import org.opentcs.data.notification.UserNotification;
import org.opentcs.kernel.persistence.ModelPersister;
import org.opentcs.kernel.workingset.PlantModelManager;
import org.opentcs.util.event.EventHandler;
/**
* This class is the standard implementation of the {@link PlantModelService} interface.
*/
public class StandardPlantModelService
extends
AbstractTCSObjectService
implements
InternalPlantModelService {
/**
* The kernel.
*/
private final Kernel kernel;
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The plant model manager.
*/
private final PlantModelManager plantModelManager;
/**
* The persister loading and storing model data.
*/
private final ModelPersister modelPersister;
/**
* Where we send events to.
*/
private final EventHandler eventHandler;
/**
* The notification service.
*/
private final NotificationService notificationService;
/**
* Creates a new instance.
*
* @param kernel The kernel.
* @param objectService The tcs object service.
* @param globalSyncObject The kernel threads' global synchronization object.
* @param plantModelManager The plant model manager to be used.
* @param modelPersister The model persister to be used.
* @param eventHandler Where this instance sends events to.
* @param notificationService The notification service.
*/
@Inject
public StandardPlantModelService(
LocalKernel kernel,
TCSObjectService objectService,
@GlobalSyncObject
Object globalSyncObject,
PlantModelManager plantModelManager,
ModelPersister modelPersister,
@ApplicationEventBus
EventHandler eventHandler,
NotificationService notificationService
) {
super(objectService);
this.kernel = requireNonNull(kernel, "kernel");
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager");
this.modelPersister = requireNonNull(modelPersister, "modelPersister");
this.eventHandler = requireNonNull(eventHandler, "eventHandler");
this.notificationService = requireNonNull(notificationService, "notificationService");
}
@Override
public Set<TCSResource<?>> expandResources(Set<TCSResourceReference<?>> resources)
throws ObjectUnknownException {
requireNonNull(resources, "resources");
synchronized (globalSyncObject) {
return plantModelManager.expandResources(resources);
}
}
@Override
public void loadPlantModel()
throws IllegalStateException {
synchronized (globalSyncObject) {
if (!modelPersister.hasSavedModel()) {
createPlantModel(new PlantModelCreationTO(Kernel.DEFAULT_MODEL_NAME));
return;
}
final String oldModelName = getModelName();
// Load the new model
PlantModelCreationTO modelCreationTO = modelPersister.readModel();
final String newModelName = isNullOrEmpty(modelCreationTO.getName())
? ""
: modelCreationTO.getName();
// Let listeners know we're in transition.
emitModelEvent(oldModelName, newModelName, true, false);
plantModelManager.createPlantModelObjects(modelCreationTO);
// Let listeners know we're done with the transition.
emitModelEvent(oldModelName, newModelName, true, true);
notificationService.publishUserNotification(
new UserNotification(
"Kernel loaded model " + newModelName,
UserNotification.Level.INFORMATIONAL
)
);
}
}
@Override
public void savePlantModel()
throws IllegalStateException {
synchronized (globalSyncObject) {
modelPersister.saveModel(plantModelManager.createPlantModelCreationTO());
}
}
@Override
public PlantModel getPlantModel() {
synchronized (globalSyncObject) {
return new PlantModel(plantModelManager.getName())
.withProperties(getModelProperties())
.withPoints(fetchObjects(Point.class))
.withPaths(fetchObjects(Path.class))
.withLocationTypes(fetchObjects(LocationType.class))
.withLocations(fetchObjects(Location.class))
.withBlocks(fetchObjects(Block.class))
.withVehicles(fetchObjects(Vehicle.class))
.withVisualLayout(fetchObjects(VisualLayout.class).stream().findFirst().get());
}
}
@Override
public void createPlantModel(PlantModelCreationTO to)
throws ObjectUnknownException,
ObjectExistsException,
IllegalStateException {
requireNonNull(to, "to");
boolean kernelInOperating = kernel.getState() == Kernel.State.OPERATING;
// If we are in state operating, change the kernel state before creating the plant model
if (kernelInOperating) {
kernel.setState(Kernel.State.MODELLING);
}
String oldModelName = getModelName();
emitModelEvent(oldModelName, to.getName(), true, false);
// Create the plant model
synchronized (globalSyncObject) {
plantModelManager.createPlantModelObjects(to);
}
savePlantModel();
// If we were in state operating before, change the kernel state back to operating
if (kernelInOperating) {
kernel.setState(Kernel.State.OPERATING);
}
emitModelEvent(oldModelName, to.getName(), true, true);
notificationService.publishUserNotification(
new UserNotification(
"Kernel created model " + to.getName(),
UserNotification.Level.INFORMATIONAL
)
);
}
@Override
public String getModelName() {
synchronized (globalSyncObject) {
return plantModelManager.getName();
}
}
@Override
public Map<String, String> getModelProperties()
throws KernelRuntimeException {
synchronized (globalSyncObject) {
return plantModelManager.getProperties();
}
}
@Override
public void updateLocationLock(TCSObjectReference<Location> ref, boolean locked)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
plantModelManager.setLocationLocked(ref, locked);
}
}
@Override
public void updatePathLock(TCSObjectReference<Path> ref, boolean locked)
throws ObjectUnknownException,
KernelRuntimeException {
synchronized (globalSyncObject) {
plantModelManager.setPathLocked(ref, locked);
}
}
/**
* Generates an event for a Model change.
*
* @param oldModelName The old model name.
* @param newModelName The new model name.
* @param modelContentChanged Whether the model's content actually changed.
* @param transitionFinished Whether the transition is finished or not.
*/
private void emitModelEvent(
String oldModelName,
String newModelName,
boolean modelContentChanged,
boolean transitionFinished
) {
requireNonNull(newModelName, "newModelName");
eventHandler.onEvent(
new ModelTransitionEvent(
oldModelName,
newModelName,
modelContentChanged,
transitionFinished
)
);
}
}

View File

@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import org.opentcs.components.kernel.Query;
import org.opentcs.components.kernel.QueryResponder;
import org.opentcs.components.kernel.services.InternalQueryService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
/**
* The default implementation of the {@link InternalQueryService} interface.
*/
public class StandardQueryService
implements
InternalQueryService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The responders, by query type.
*/
private final Map<Class<? extends Query<?>>, QueryResponder> respondersByQueryType
= new HashMap<>();
/**
* Creates a new instance.
*
* @param globalSyncObject The kernel threads' global synchronization object.
*/
@Inject
public StandardQueryService(
@GlobalSyncObject
Object globalSyncObject
) {
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
}
@Override
public <T> T query(Query<T> query) {
requireNonNull(query, "query");
synchronized (globalSyncObject) {
QueryResponder responder = respondersByQueryType.get(query.getClass());
checkArgument(responder != null, "Query class not taken: %s", query.getClass().getName());
return responder.query(query);
}
}
@Override
public void registerResponder(
@Nonnull
Class<? extends Query<?>> clazz,
@Nonnull
QueryResponder responder
) {
requireNonNull(clazz, "clazz");
requireNonNull(responder, "responder");
synchronized (globalSyncObject) {
checkArgument(
!respondersByQueryType.containsKey(clazz),
"Query class already taken: %s",
clazz.getName()
);
respondersByQueryType.put(clazz, responder);
}
}
@Override
public void unregisterResponder(
@Nonnull
Class<? extends Query<?>> clazz
) {
requireNonNull(clazz, "clazz");
synchronized (globalSyncObject) {
respondersByQueryType.remove(clazz);
}
}
}

View File

@@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.opentcs.access.KernelRuntimeException;
import org.opentcs.components.kernel.Router;
import org.opentcs.components.kernel.services.RouterService;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Path;
import org.opentcs.data.model.Point;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.order.Route;
import org.opentcs.kernel.workingset.PlantModelManager;
/**
* This class is the standard implementation of the {@link RouterService} interface.
*/
public class StandardRouterService
implements
RouterService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The router.
*/
private final Router router;
/**
* The plant model manager.
*/
private final PlantModelManager plantModelManager;
/**
* The object service.
*/
private final TCSObjectService objectService;
/**
* Creates a new instance.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param router The scheduler.
* @param plantModelManager The plant model manager to be used.
* @param objectService The object service.
*/
@Inject
public StandardRouterService(
@GlobalSyncObject
Object globalSyncObject,
Router router,
PlantModelManager plantModelManager,
TCSObjectService objectService
) {
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.router = requireNonNull(router, "router");
this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager");
this.objectService = requireNonNull(objectService, "objectService");
}
@Override
public void updateRoutingTopology(Set<TCSObjectReference<Path>> refs)
throws KernelRuntimeException {
synchronized (globalSyncObject) {
router.updateRoutingTopology(
refs.stream()
.map(ref -> plantModelManager.getObjectRepo().getObject(Path.class, ref))
.collect(Collectors.toSet())
);
}
}
@Override
public Map<TCSObjectReference<Point>, Route> computeRoutes(
TCSObjectReference<Vehicle> vehicleRef,
TCSObjectReference<Point> sourcePointRef,
Set<TCSObjectReference<Point>> destinationPointRefs,
Set<TCSResourceReference<?>> resourcesToAvoid
) {
requireNonNull(vehicleRef, "vehicleRef");
requireNonNull(sourcePointRef, "sourcePointRef");
requireNonNull(destinationPointRefs, "destinationPointRefs");
requireNonNull(resourcesToAvoid, "resourcesToAvoid");
synchronized (globalSyncObject) {
Map<TCSObjectReference<Point>, Route> result = new HashMap<>();
Vehicle vehicle = objectService.fetchObject(Vehicle.class, vehicleRef);
if (vehicle == null) {
throw new ObjectUnknownException("Unknown vehicle: " + vehicleRef.getName());
}
Point sourcePoint = objectService.fetchObject(Point.class, sourcePointRef);
if (sourcePoint == null) {
throw new ObjectUnknownException("Unknown source point: " + sourcePointRef.getName());
}
for (TCSObjectReference<Point> dest : destinationPointRefs) {
Point destinationPoint = objectService.fetchObject(Point.class, dest);
if (destinationPoint == null) {
throw new ObjectUnknownException("Unknown destination point: " + dest.getName());
}
result.put(
dest,
router.getRoute(vehicle, sourcePoint, destinationPoint, resourcesToAvoid)
.orElse(null)
);
}
return result;
}
}
}

View File

@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectHistory;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObject;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.kernel.workingset.TCSObjectManager;
import org.opentcs.kernel.workingset.TCSObjectRepository;
/**
* This class is the standard implementation of the {@link TCSObjectService} interface.
*/
public class StandardTCSObjectService
implements
TCSObjectService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The object manager.
*/
private final TCSObjectManager objectManager;
/**
* Creates a new instance.
*
* @param globalSyncObject The kernel threads' global synchronization object.
* @param objectManager The object manager.
*/
@Inject
public StandardTCSObjectService(
@GlobalSyncObject
Object globalSyncObject,
TCSObjectManager objectManager
) {
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.objectManager = requireNonNull(objectManager, "objectManager");
}
@Override
public <T extends TCSObject<T>> T fetchObject(Class<T> clazz, TCSObjectReference<T> ref) {
requireNonNull(clazz, "clazz");
requireNonNull(ref, "ref");
synchronized (getGlobalSyncObject()) {
return getObjectRepo().getObjectOrNull(clazz, ref);
}
}
@Override
public <T extends TCSObject<T>> T fetchObject(Class<T> clazz, String name) {
requireNonNull(clazz, "clazz");
synchronized (getGlobalSyncObject()) {
return getObjectRepo().getObjectOrNull(clazz, name);
}
}
@Override
public <T extends TCSObject<T>> Set<T> fetchObjects(Class<T> clazz) {
requireNonNull(clazz, "clazz");
synchronized (getGlobalSyncObject()) {
Set<T> objects = getObjectRepo().getObjects(clazz);
Set<T> copies = new HashSet<>();
for (T object : objects) {
copies.add(object);
}
return copies;
}
}
@Override
public <T extends TCSObject<T>> Set<T> fetchObjects(
@Nonnull
Class<T> clazz,
@Nonnull
Predicate<? super T> predicate
) {
requireNonNull(clazz, "clazz");
requireNonNull(predicate, "predicate");
synchronized (getGlobalSyncObject()) {
return getObjectRepo().getObjects(clazz, predicate);
}
}
@Override
public void updateObjectProperty(
TCSObjectReference<?> ref,
String key,
@Nullable
String value
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(key, "key");
synchronized (getGlobalSyncObject()) {
objectManager.setObjectProperty(ref, key, value);
}
}
@Override
public void appendObjectHistoryEntry(TCSObjectReference<?> ref, ObjectHistory.Entry entry)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(entry, "entry");
synchronized (getGlobalSyncObject()) {
objectManager.appendObjectHistoryEntry(ref, entry);
}
}
protected Object getGlobalSyncObject() {
return globalSyncObject;
}
protected TCSObjectRepository getObjectRepo() {
return objectManager.getObjectRepo();
}
}

View File

@@ -0,0 +1,257 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.List;
import org.opentcs.access.to.order.OrderSequenceCreationTO;
import org.opentcs.access.to.order.TransportOrderCreationTO;
import org.opentcs.components.kernel.services.InternalTransportOrderService;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.components.kernel.services.TransportOrderService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectExistsException;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.order.DriveOrder;
import org.opentcs.data.order.OrderSequence;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.kernel.workingset.PlantModelManager;
import org.opentcs.kernel.workingset.TCSObjectRepository;
import org.opentcs.kernel.workingset.TransportOrderPoolManager;
/**
* This class is the standard implementation of the {@link TransportOrderService} interface.
*/
public class StandardTransportOrderService
extends
AbstractTCSObjectService
implements
InternalTransportOrderService {
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* The container of all course model and transport order objects.
*/
private final TCSObjectRepository globalObjectPool;
/**
* The order pool manager.
*/
private final TransportOrderPoolManager orderPoolManager;
/**
* The plant model manager.
*/
private final PlantModelManager plantModelManager;
/**
* Creates a new instance.
*
* @param objectService The tcs obejct service.
* @param globalSyncObject The kernel threads' global synchronization object.
* @param globalObjectPool The object pool to be used.
* @param orderPoolManager The order pool manager to be used.
* @param plantModelManager The plant model manager to be used.
*/
@Inject
public StandardTransportOrderService(
TCSObjectService objectService,
@GlobalSyncObject
Object globalSyncObject,
TCSObjectRepository globalObjectPool,
TransportOrderPoolManager orderPoolManager,
PlantModelManager plantModelManager
) {
super(objectService);
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.globalObjectPool = requireNonNull(globalObjectPool, "globalObjectPool");
this.orderPoolManager = requireNonNull(orderPoolManager, "orderPoolManager");
this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager");
}
@Override
public void markOrderSequenceFinished(TCSObjectReference<OrderSequence> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
OrderSequence seq = globalObjectPool.getObject(OrderSequence.class, ref);
// Make sure we don't execute this if the sequence is already marked as finished, as that
// would make it possible to trigger disposition of a vehicle at any given moment.
if (seq.isFinished()) {
return;
}
orderPoolManager.setOrderSequenceFinished(ref);
// If the sequence was being processed by a vehicle, clear its back reference to the sequence
// to make it available again and dispatch it.
if (seq.getProcessingVehicle() != null) {
Vehicle vehicle = globalObjectPool.getObject(
Vehicle.class,
seq.getProcessingVehicle()
);
plantModelManager.setVehicleOrderSequence(vehicle.getReference(), null);
}
}
}
@Override
public void updateOrderSequenceFinishedIndex(TCSObjectReference<OrderSequence> ref, int index)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
orderPoolManager.setOrderSequenceFinishedIndex(ref, index);
}
}
@Override
public void updateOrderSequenceProcessingVehicle(
TCSObjectReference<OrderSequence> seqRef,
TCSObjectReference<Vehicle> vehicleRef
)
throws ObjectUnknownException {
requireNonNull(seqRef, "seqRef");
synchronized (globalSyncObject) {
orderPoolManager.setOrderSequenceProcessingVehicle(seqRef, vehicleRef);
}
}
@Override
public void updateTransportOrderProcessingVehicle(
TCSObjectReference<TransportOrder> orderRef,
TCSObjectReference<Vehicle> vehicleRef,
List<DriveOrder> driveOrders
)
throws ObjectUnknownException,
IllegalArgumentException {
requireNonNull(orderRef, "orderRef");
requireNonNull(driveOrders, "driveOrders");
synchronized (globalSyncObject) {
orderPoolManager.setTransportOrderProcessingVehicle(orderRef, vehicleRef, driveOrders);
}
}
@Override
public void updateTransportOrderDriveOrders(
TCSObjectReference<TransportOrder> ref,
List<DriveOrder> driveOrders
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(driveOrders, "driveOrders");
synchronized (globalSyncObject) {
orderPoolManager.setTransportOrderDriveOrders(ref, driveOrders);
}
}
@Override
public void updateTransportOrderNextDriveOrder(TCSObjectReference<TransportOrder> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
orderPoolManager.setTransportOrderNextDriveOrder(ref);
}
}
@Override
public void updateTransportOrderCurrentRouteStepIndex(
TCSObjectReference<TransportOrder> ref,
int index
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
orderPoolManager.setTransportOrderCurrentRouteStepIndex(ref, index);
}
}
@Override
public void updateTransportOrderState(
TCSObjectReference<TransportOrder> ref,
TransportOrder.State state
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(state, "state");
synchronized (globalSyncObject) {
orderPoolManager.setTransportOrderState(ref, state);
}
}
@Override
public OrderSequence createOrderSequence(OrderSequenceCreationTO to) {
requireNonNull(to, "to");
synchronized (globalSyncObject) {
return orderPoolManager.createOrderSequence(to);
}
}
@Override
public TransportOrder createTransportOrder(TransportOrderCreationTO to)
throws ObjectUnknownException,
ObjectExistsException {
requireNonNull(to, "to");
synchronized (globalSyncObject) {
return orderPoolManager.createTransportOrder(to);
}
}
@Override
public void markOrderSequenceComplete(TCSObjectReference<OrderSequence> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
OrderSequence seq = globalObjectPool.getObject(OrderSequence.class, ref);
// Make sure we don't execute this if the sequence is already marked as finished, as that
// would make it possible to trigger disposition of a vehicle at any given moment.
if (seq.isComplete()) {
return;
}
orderPoolManager.setOrderSequenceComplete(ref);
// If there aren't any transport orders left to be processed as part of the sequence, mark
// it as finished, too.
if (seq.getNextUnfinishedOrder() == null) {
orderPoolManager.setOrderSequenceFinished(ref);
// If the sequence was being processed by a vehicle, clear its back reference to the
// sequence to make it available again and dispatch it.
if (seq.getProcessingVehicle() != null) {
Vehicle vehicle = globalObjectPool.getObject(
Vehicle.class,
seq.getProcessingVehicle()
);
plantModelManager.setVehicleOrderSequence(vehicle.getReference(), null);
}
}
}
}
@Override
public void updateTransportOrderIntendedVehicle(
TCSObjectReference<TransportOrder> orderRef,
TCSObjectReference<Vehicle> vehicleRef
)
throws ObjectUnknownException,
IllegalArgumentException {
requireNonNull(orderRef, "orderRef");
synchronized (globalSyncObject) {
orderPoolManager.setTransportOrderIntendedVehicle(orderRef, vehicleRef);
}
}
}

View File

@@ -0,0 +1,504 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.services;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Set;
import org.opentcs.access.KernelRuntimeException;
import org.opentcs.components.kernel.services.InternalVehicleService;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.components.kernel.services.VehicleService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.BoundingBox;
import org.opentcs.data.model.Point;
import org.opentcs.data.model.Pose;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.model.Triple;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.model.Vehicle.EnergyLevelThresholdSet;
import org.opentcs.data.order.OrderSequence;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.drivers.vehicle.AdapterCommand;
import org.opentcs.drivers.vehicle.LoadHandlingDevice;
import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription;
import org.opentcs.drivers.vehicle.management.VehicleAttachmentInformation;
import org.opentcs.drivers.vehicle.management.VehicleProcessModelTO;
import org.opentcs.kernel.extensions.controlcenter.vehicles.AttachmentManager;
import org.opentcs.kernel.extensions.controlcenter.vehicles.VehicleEntry;
import org.opentcs.kernel.extensions.controlcenter.vehicles.VehicleEntryPool;
import org.opentcs.kernel.vehicles.LocalVehicleControllerPool;
import org.opentcs.kernel.vehicles.VehicleCommAdapterRegistry;
import org.opentcs.kernel.workingset.PlantModelManager;
import org.opentcs.util.annotations.ScheduledApiChange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is the standard implementation of the {@link VehicleService} interface.
*/
public class StandardVehicleService
extends
AbstractTCSObjectService
implements
InternalVehicleService {
/**
* This class' logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(StandardVehicleService.class);
/**
* A global object to be used for synchronization within the kernel.
*/
private final Object globalSyncObject;
/**
* A pool of vehicle controllers.
*/
private final LocalVehicleControllerPool vehicleControllerPool;
/**
* A pool of vehicle entries.
*/
private final VehicleEntryPool vehicleEntryPool;
/**
* The attachment manager.
*/
private final AttachmentManager attachmentManager;
/**
* The registry for all communication adapters.
*/
private final VehicleCommAdapterRegistry commAdapterRegistry;
/**
* The plant model manager.
*/
private final PlantModelManager plantModelManager;
/**
* Creates a new instance.
*
* @param objectService The tcs object service.
* @param globalSyncObject The kernel threads' global synchronization object.
* @param vehicleControllerPool The controller pool to be used.
* @param vehicleEntryPool The pool of vehicle entries to be used.
* @param attachmentManager The attachment manager.
* @param commAdapterRegistry The registry for all communication adapters.
* @param plantModelManager The plant model manager to be used.
*/
@Inject
public StandardVehicleService(
TCSObjectService objectService,
@GlobalSyncObject
Object globalSyncObject,
LocalVehicleControllerPool vehicleControllerPool,
VehicleEntryPool vehicleEntryPool,
AttachmentManager attachmentManager,
VehicleCommAdapterRegistry commAdapterRegistry,
PlantModelManager plantModelManager
) {
super(objectService);
this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
this.vehicleControllerPool = requireNonNull(vehicleControllerPool, "vehicleControllerPool");
this.vehicleEntryPool = requireNonNull(vehicleEntryPool, "vehicleEntryPool");
this.attachmentManager = requireNonNull(attachmentManager, "attachmentManager");
this.commAdapterRegistry = requireNonNull(commAdapterRegistry, "commAdapterRegistry");
this.plantModelManager = requireNonNull(plantModelManager, "plantModelManager");
}
@Override
public void updateVehicleEnergyLevel(TCSObjectReference<Vehicle> ref, int energyLevel)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
plantModelManager.setVehicleEnergyLevel(ref, energyLevel);
}
}
@Override
public void updateVehicleLoadHandlingDevices(
TCSObjectReference<Vehicle> ref,
List<LoadHandlingDevice> devices
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(devices, "devices");
synchronized (globalSyncObject) {
plantModelManager.setVehicleLoadHandlingDevices(ref, devices);
}
}
@Override
public void updateVehicleNextPosition(
TCSObjectReference<Vehicle> vehicleRef,
TCSObjectReference<Point> pointRef
)
throws ObjectUnknownException {
requireNonNull(vehicleRef, "vehicleRef");
synchronized (globalSyncObject) {
plantModelManager.setVehicleNextPosition(vehicleRef, pointRef);
}
}
@Override
public void updateVehicleOrderSequence(
TCSObjectReference<Vehicle> vehicleRef,
TCSObjectReference<OrderSequence> sequenceRef
)
throws ObjectUnknownException {
requireNonNull(vehicleRef, "vehicleRef");
synchronized (globalSyncObject) {
plantModelManager.setVehicleOrderSequence(vehicleRef, sequenceRef);
}
}
@Deprecated
@Override
public void updateVehicleOrientationAngle(TCSObjectReference<Vehicle> ref, double angle)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
Vehicle previousState = plantModelManager.getObjectRepo().getObject(Vehicle.class, ref);
plantModelManager.setVehiclePose(ref, previousState.getPose().withOrientationAngle(angle));
}
}
@Override
public void updateVehiclePosition(
TCSObjectReference<Vehicle> vehicleRef,
TCSObjectReference<Point> pointRef
)
throws ObjectUnknownException {
requireNonNull(vehicleRef, "vehicleRef");
synchronized (globalSyncObject) {
LOG.debug("Vehicle {} has reached point {}.", vehicleRef, pointRef);
plantModelManager.setVehiclePosition(vehicleRef, pointRef);
}
}
@Deprecated
@Override
public void updateVehiclePrecisePosition(TCSObjectReference<Vehicle> ref, Triple position)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
Vehicle previousState = plantModelManager.getObjectRepo().getObject(Vehicle.class, ref);
plantModelManager.setVehiclePose(ref, previousState.getPose().withPosition(position));
}
}
@Override
public void updateVehiclePose(TCSObjectReference<Vehicle> ref, Pose pose)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(pose, "pose");
synchronized (globalSyncObject) {
plantModelManager.setVehiclePose(ref, pose);
}
}
@Override
public void updateVehicleProcState(TCSObjectReference<Vehicle> ref, Vehicle.ProcState state)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(state, "state");
synchronized (globalSyncObject) {
LOG.debug("Updating procState of vehicle {} to {}...", ref.getName(), state);
plantModelManager.setVehicleProcState(ref, state);
}
}
@Override
public void updateVehicleRechargeOperation(
TCSObjectReference<Vehicle> ref,
String rechargeOperation
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(rechargeOperation, "rechargeOperation");
synchronized (globalSyncObject) {
plantModelManager.setVehicleRechargeOperation(ref, rechargeOperation);
}
}
@Override
public void updateVehicleClaimedResources(
TCSObjectReference<Vehicle> ref,
List<Set<TCSResourceReference<?>>> resources
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(resources, "resources");
synchronized (globalSyncObject) {
plantModelManager.setVehicleClaimedResources(ref, resources);
}
}
@Override
public void updateVehicleAllocatedResources(
TCSObjectReference<Vehicle> ref,
List<Set<TCSResourceReference<?>>> resources
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(resources, "resources");
synchronized (globalSyncObject) {
plantModelManager.setVehicleAllocatedResources(ref, resources);
}
}
@Override
public void updateVehicleState(TCSObjectReference<Vehicle> ref, Vehicle.State state)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(state, "state");
synchronized (globalSyncObject) {
plantModelManager.setVehicleState(ref, state);
}
}
@Override
@Deprecated
public void updateVehicleLength(TCSObjectReference<Vehicle> ref, int length)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
plantModelManager.setVehicleBoundingBox(
ref,
plantModelManager.getObjectRepo().getObject(Vehicle.class, ref)
.getBoundingBox()
.withLength(length)
);
}
}
@Override
public void updateVehicleTransportOrder(
TCSObjectReference<Vehicle> vehicleRef,
TCSObjectReference<TransportOrder> orderRef
)
throws ObjectUnknownException {
requireNonNull(vehicleRef, "vehicleRef");
synchronized (globalSyncObject) {
plantModelManager.setVehicleTransportOrder(vehicleRef, orderRef);
}
}
@Override
public void attachCommAdapter(
TCSObjectReference<Vehicle> ref,
VehicleCommAdapterDescription description
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(description, "description");
synchronized (globalSyncObject) {
attachmentManager.attachAdapterToVehicle(
ref.getName(),
commAdapterRegistry.findFactoryFor(description)
);
}
}
@Override
public void disableCommAdapter(TCSObjectReference<Vehicle> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
VehicleEntry entry = vehicleEntryPool.getEntryFor(ref.getName());
if (entry == null) {
throw new IllegalArgumentException("No vehicle entry found for" + ref.getName());
}
entry.getCommAdapter().disable();
}
}
@Override
public void enableCommAdapter(TCSObjectReference<Vehicle> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
VehicleEntry entry = vehicleEntryPool.getEntryFor(ref.getName());
if (entry == null) {
throw new IllegalArgumentException("No vehicle entry found for " + ref.getName());
}
entry.getCommAdapter().enable();
}
}
@Override
public VehicleAttachmentInformation fetchAttachmentInformation(TCSObjectReference<Vehicle> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
return attachmentManager.getAttachmentInformation(ref.getName());
}
}
@Override
public VehicleProcessModelTO fetchProcessModel(TCSObjectReference<Vehicle> ref)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
VehicleEntry entry = vehicleEntryPool.getEntryFor(ref.getName());
if (entry == null) {
throw new IllegalArgumentException("No vehicle entry found for " + ref.getName());
}
return entry.getCommAdapter().createTransferableProcessModel();
}
}
@Override
public void sendCommAdapterCommand(TCSObjectReference<Vehicle> ref, AdapterCommand command)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(command, "command");
synchronized (globalSyncObject) {
vehicleControllerPool
.getVehicleController(ref.getName())
.sendCommAdapterCommand(command);
}
}
@Override
public void sendCommAdapterMessage(TCSObjectReference<Vehicle> ref, Object message)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
vehicleControllerPool
.getVehicleController(ref.getName())
.sendCommAdapterMessage(message);
}
}
@Override
public void updateVehicleIntegrationLevel(
TCSObjectReference<Vehicle> ref,
Vehicle.IntegrationLevel integrationLevel
)
throws ObjectUnknownException,
KernelRuntimeException {
requireNonNull(ref, "ref");
requireNonNull(integrationLevel, "integrationLevel");
synchronized (globalSyncObject) {
Vehicle vehicle = fetchObject(Vehicle.class, ref);
if (vehicle.isProcessingOrder()
&& (integrationLevel == Vehicle.IntegrationLevel.TO_BE_IGNORED
|| integrationLevel == Vehicle.IntegrationLevel.TO_BE_NOTICED)) {
throw new IllegalArgumentException(
String.format(
"%s: Cannot change integration level to %s while processing orders.",
vehicle.getName(),
integrationLevel.name()
)
);
}
plantModelManager.setVehicleIntegrationLevel(ref, integrationLevel);
}
}
@Override
public void updateVehiclePaused(TCSObjectReference<Vehicle> ref, boolean paused)
throws ObjectUnknownException,
KernelRuntimeException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
plantModelManager.setVehiclePaused(ref, paused);
vehicleControllerPool.getVehicleController(ref.getName()).onVehiclePaused(paused);
}
}
@Override
public void updateVehicleEnergyLevelThresholdSet(
TCSObjectReference<Vehicle> ref,
EnergyLevelThresholdSet energyLevelThresholdSet
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(energyLevelThresholdSet, "energyLevelThresholdSet");
synchronized (globalSyncObject) {
plantModelManager.setVehicleEnergyLevelThresholdSet(ref, energyLevelThresholdSet);
}
}
@Override
public void updateVehicleAllowedOrderTypes(
TCSObjectReference<Vehicle> ref,
Set<String> allowedOrderTypes
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(allowedOrderTypes, "allowedOrderTypes");
synchronized (globalSyncObject) {
plantModelManager.setVehicleAllowedOrderTypes(ref, allowedOrderTypes);
}
}
@Override
@ScheduledApiChange(when = "7.0", details = "Envelope key will become non-null.")
public void updateVehicleEnvelopeKey(TCSObjectReference<Vehicle> ref, String envelopeKey)
throws ObjectUnknownException,
IllegalArgumentException,
KernelRuntimeException {
requireNonNull(ref, "ref");
synchronized (globalSyncObject) {
Vehicle vehicle = fetchObject(Vehicle.class, ref);
if (vehicle.isProcessingOrder()
|| !vehicle.getClaimedResources().isEmpty()
|| !vehicle.getAllocatedResources().isEmpty()) {
throw new IllegalArgumentException(
"Updating a vehicle's envelope key while the vehicle is processing an order or "
+ "claiming/allocating resources is currently not supported."
);
}
plantModelManager.setVehicleEnvelopeKey(ref, envelopeKey);
}
}
@Override
public void updateVehicleBoundingBox(TCSObjectReference<Vehicle> ref, BoundingBox boundingBox)
throws ObjectUnknownException,
KernelRuntimeException {
requireNonNull(ref, "ref");
requireNonNull(boundingBox, "boundingBox");
synchronized (globalSyncObject) {
plantModelManager.setVehicleBoundingBox(ref, boundingBox);
}
}
}

View File

@@ -0,0 +1,772 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import static org.opentcs.util.Assertions.checkState;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.SequencedCollection;
import java.util.Set;
import java.util.stream.Collectors;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.Path;
import org.opentcs.data.model.Point;
import org.opentcs.data.model.TCSResource;
import org.opentcs.drivers.vehicle.MovementCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tracks processing of movement commands.
* <p>
* After the movement commands for a new or updated drive order have been passed to
* {@link #driveOrderUpdated(SequencedCollection)}, movement commands and their corresponding
* resources are expected to be processed in the following order:
* <ol>
* <li>{@link #allocationRequested(Set)}</li>
* <li>{@link #allocationConfirmed(Set)}</li>
* <li>{@link #commandSent(MovementCommand)}</li>
* <li>{@link #commandExecuted(MovementCommand)}</li>
* <li>{@link #allocationReleased(Set)}</li>
* </ol>
*/
public class CommandProcessingTracker {
private static final Logger LOG = LoggerFactory.getLogger(CommandProcessingTracker.class);
/**
* The queue of commands that still need to be sent to the communication adapter.
*/
private final Deque<CommandResourcePair> futureCommands = new ArrayDeque<>();
/**
* A command (the next one for the current drive order) that has yet to be sent to the
* communication adapter.
*/
private CommandResourcePair pendingCommand;
/**
* The state the pending command is currently in.
*/
private PendingCommandState pendingCommandState = PendingCommandState.UNDEFINED;
/**
* The queue of commands that have been sent to the communication adapter.
*/
private final Deque<CommandResourcePair> sentCommands = new ArrayDeque<>();
/**
* The command that was executed last.
*/
private MovementCommand lastCommandExecuted;
/**
* The queue of resource sets that the vehicle has already passed.
* <p>
* For every movement command that is executed, two elements are added to this queue - one element
* containing only the path (if any) associated with the respective movement command, and another
* element containing the point and the location (if any) associated with the respective movement
* command.
* </p>
*/
private final Deque<Set<TCSResource<?>>> passedResources = new ArrayDeque<>();
/**
* Creates a new instance.
*/
public CommandProcessingTracker() {
}
/**
* Clears (i.e. resets) this instance.
*/
public void clear() {
futureCommands.clear();
pendingCommand = null;
pendingCommandState = PendingCommandState.UNDEFINED;
sentCommands.clear();
lastCommandExecuted = null;
passedResources.clear();
}
/**
* Called when a drive order was updated (either because a new one was assigned to a vehicle or
* the one being currently processed was updated due to rerouting).
*
* @param movementCommands The collection of movement commands that belong to the updated drive
* order.
*/
public void driveOrderUpdated(
@Nonnull
SequencedCollection<MovementCommand> movementCommands
) {
requireNonNull(movementCommands, "movementCommands");
if (isDriveOrderFinished()) {
// The movement commands belong to a new drive order.
futureCommands.addAll(toCommandResourcePairs(movementCommands));
}
else {
// The movement commands belong to the same drive order we are currently processing.
futureCommands.clear();
if (pendingCommandState == PendingCommandState.ALLOCATION_PENDING) {
// With drive order updates, any pending resource allocation is reset.
pendingCommand = null;
pendingCommandState = PendingCommandState.UNDEFINED;
}
futureCommands.addAll(toCommandResourcePairs(movementCommands));
// The current drive order got updated but our queue of future commands now contains commands
// that have already been processed, so discard these.
discardProcessedFutureCommands();
}
}
/**
* Called when a drive order was aborted.
*
* @param immediate Indicates whether the drive order was aborted immediately or regularly.
*/
public void driveOrderAborted(boolean immediate) {
if (immediate) {
futureCommands.clear();
pendingCommand = null;
pendingCommandState = PendingCommandState.UNDEFINED;
sentCommands.clear();
}
else {
futureCommands.clear();
if (pendingCommandState != PendingCommandState.SENDING_PENDING) {
pendingCommand = null;
pendingCommandState = PendingCommandState.UNDEFINED;
}
}
}
/**
* Checks if there are any movement commands that are yet to be sent to the communication adapter.
*
* @return {@code true} if there are commands to be sent, otherwise {@code false}.
*/
public boolean hasCommandsToBeSent() {
return !futureCommands.isEmpty()
|| pendingCommandState == PendingCommandState.ALLOCATION_PENDING
|| pendingCommandState == PendingCommandState.SENDING_PENDING;
}
/**
* Checks if the current drive order is considered finished.
*
* @return {@code true}, if there are no more commands that need to be sent to the communication
* adapter and all commands already sent have been reported as executed.
*/
public boolean isDriveOrderFinished() {
return futureCommands.isEmpty()
&& pendingCommand == null
&& sentCommands.isEmpty();
}
/**
* Called when a vehicle's allocation was reset.
*
* @param resources The (only) set of resources that the vehicle now allocates.
*/
public void allocationReset(
@Nonnull
Set<TCSResource<?>> resources
) {
requireNonNull(resources, "resources");
// Clear resources that have been passed previously as they are no longer allocated.
passedResources.clear();
if (!resources.isEmpty()) {
// Now, the given resources are allocated and considered as the new passed resources.
passedResources.add(resources);
}
// Discard the pending command since pending allocations are reset and resources that have
// already been allocated are freed when allocation is reset.
pendingCommand = null;
pendingCommandState = PendingCommandState.UNDEFINED;
// Clear sent commands since we don't expect a vehicle to report these commands as executed
// after allocation has been reset.
sentCommands.clear();
}
/**
* Called when a resource allocation was requested.
*
* @param resources The resources for which allocation was requested.
*/
public void allocationRequested(
@Nonnull
Set<TCSResource<?>> resources
) {
requireNonNull(resources, "resources");
checkArgument(
!futureCommands.isEmpty(),
"Allocation requested, but there are no future commands: %s",
resources
);
checkArgument(
Objects.equals(futureCommands.peek().getResources(), resources),
"Resource set is not head of future commands: %s (futureCommands=%s)",
resources,
futureCommands
);
checkArgument(
pendingCommandState == PendingCommandState.UNDEFINED,
"pendingCommandState is not '%s' but '%s'",
PendingCommandState.UNDEFINED,
pendingCommandState
);
pendingCommand = futureCommands.remove();
pendingCommandState = PendingCommandState.ALLOCATION_PENDING;
}
/**
* Called when a resource allocation was confirmed.
*
* @param resources The resources for which allocation was confirmed.
*/
public void allocationConfirmed(
@Nonnull
Set<TCSResource<?>> resources
) {
requireNonNull(resources, "resources");
checkArgument(
pendingCommandState == PendingCommandState.ALLOCATION_PENDING,
"pendingCommandState is not '%s' but '%s'",
PendingCommandState.ALLOCATION_PENDING,
pendingCommandState
);
checkArgument(
Objects.equals(pendingCommand.getResources(), resources),
"Resource set does not belong to pending command: %s (pendingCommand=%s)",
resources,
pendingCommand
);
pendingCommandState = PendingCommandState.SENDING_PENDING;
}
/**
* Called when a resource allocation was revoked.
*
* @param resources The resources for which allocation was revoked.
*/
public void allocationRevoked(
@Nonnull
Set<TCSResource<?>> resources
) {
requireNonNull(resources, "resources");
checkArgument(
pendingCommandState == PendingCommandState.SENDING_PENDING,
"pendingCommandState is not '%s' but '%s'",
PendingCommandState.SENDING_PENDING,
pendingCommandState
);
checkArgument(
Objects.equals(pendingCommand.getResources(), resources),
"Resource set does not belong to pending command: %s (pendingCommand=%s)",
resources,
pendingCommand
);
pendingCommand = null;
pendingCommandState = PendingCommandState.UNDEFINED;
}
/**
* Called when a movement command won't be sent to the communication adapter.
*
* @param movementCommand The movement command.
*/
public void commandSendingStopped(
@Nonnull
MovementCommand movementCommand
) {
requireNonNull(movementCommand, "movementCommand");
checkArgument(
pendingCommandState == PendingCommandState.SENDING_PENDING,
"pendingCommandState is not '%s' but '%s'",
PendingCommandState.SENDING_PENDING,
pendingCommandState
);
checkArgument(
Objects.equals(pendingCommand.getMovementCommand(), movementCommand),
"Movement command does not belong to pending command: %s (pendingCommand=%s)",
movementCommand,
pendingCommand
);
pendingCommandState = PendingCommandState.WONT_SEND;
}
/**
* Called when a movement command was sent to the communication adapter.
*
* @param movementCommand The movement command.
*/
public void commandSent(
@Nonnull
MovementCommand movementCommand
) {
requireNonNull(movementCommand, "movementCommand");
checkArgument(
pendingCommandState == PendingCommandState.SENDING_PENDING,
"pendingCommandState is not '%s' but '%s'",
PendingCommandState.SENDING_PENDING,
pendingCommandState
);
checkArgument(
Objects.equals(pendingCommand.getMovementCommand(), movementCommand),
"Movement command does not belong to pending command: %s (pendingCommand=%s)",
movementCommand,
pendingCommand
);
sentCommands.add(pendingCommand);
pendingCommand = null;
pendingCommandState = PendingCommandState.UNDEFINED;
}
/**
* Called when a movement command was reported as executed.
*
* @param movementCommand The movement command.
*/
public void commandExecuted(
@Nonnull
MovementCommand movementCommand
) {
requireNonNull(movementCommand, "movementCommand");
checkArgument(
!sentCommands.isEmpty(),
"Movement command reported as executed, but no commands have been sent: %s",
movementCommand
);
MovementCommand expectedCommand = sentCommands.peek().getMovementCommand();
checkArgument(
Objects.equals(expectedCommand, movementCommand),
"%s: Unexpected movement command executed: %s != %s",
movementCommand.getTransportOrder().getProcessingVehicle().getName(),
movementCommand,
expectedCommand
);
CommandResourcePair executedCommand = sentCommands.remove();
lastCommandExecuted = executedCommand.getMovementCommand();
passedResources.add(extractPath(executedCommand.getResources()));
passedResources.add(extractPointAndLocation(executedCommand.getResources()));
}
/**
* Called when a resource allocation was released.
*
* @param resources The resources for which allocation was released.
*/
public void allocationReleased(
@Nonnull
Set<TCSResource<?>> resources
) {
requireNonNull(resources, "resources");
checkArgument(
Objects.equals(passedResources.peek(), resources),
"Resource set is not head of passed resources: %s (passedResources=%s)",
resources,
passedResources
);
passedResources.remove();
}
/**
* Returns the queue of resources claimed by the vehicle.
* <p>
* The order of the elements in this queue corresponds to the order in which they will be
* allocated, with the first element in the queue (i.e. its head) corresponding to the resources
* that will be allocated next.
* </p>
*
* @return The queue of resources claimed by the vehicle.
*/
@Nonnull
public Deque<Set<TCSResource<?>>> getClaimedResources() {
Deque<Set<TCSResource<?>>> claimedResources = new ArrayDeque<>();
if (pendingCommandState == PendingCommandState.ALLOCATION_PENDING) {
claimedResources.add(pendingCommand.getResources());
}
futureCommands.stream()
.map(CommandResourcePair::getResources)
.forEach(claimedResources::add);
return claimedResources;
}
/**
* Returns the queue of resources allocated by the vehicle.
* <p>
* The order of the elements in this queue corresponds to the order in which they were allocated,
* with the first element in the queue (i.e. its head) corresponding to the oldest resources.
* </p>
*
* @return The queue of resources allocated by the vehicle.
*/
@Nonnull
public Deque<Set<TCSResource<?>>> getAllocatedResources() {
Deque<Set<TCSResource<?>>> allocatedResources = new ArrayDeque<>();
allocatedResources.addAll(passedResources);
allocatedResources.addAll(getAllocatedResourcesAhead());
return allocatedResources;
}
/**
* Returns the queue of resources allocated by the vehicle that lie in front of it.
* <p>
* The order of the elements in this queue corresponds to the order in which they were allocated,
* with the first element in the queue (i.e. its head) corresponding to the resources right in
* front of the vehicle.
* </p>
*
* @return The queue of allocated resources in front of the vehicle.
*/
@Nonnull
public Deque<Set<TCSResource<?>>> getAllocatedResourcesAhead() {
Deque<Set<TCSResource<?>>> allocatedResourcesAhead = new ArrayDeque<>();
sentCommands.stream()
.map(CommandResourcePair::getResources)
.forEach(allocatedResourcesAhead::add);
if (pendingCommandState == PendingCommandState.SENDING_PENDING
|| pendingCommandState == PendingCommandState.WONT_SEND) {
allocatedResourcesAhead.add(pendingCommand.getResources());
}
return allocatedResourcesAhead;
}
/**
* Returns the movement command for which resource allocation is currently pending.
*
* @return An optional containing the movement command for which resource allocation is currently
* pending or {@link Optional#empty()} if there is no such command.
* @see #getAllocationPendingResources()
*/
public Optional<MovementCommand> getAllocationPendingCommand() {
if (pendingCommandState == PendingCommandState.ALLOCATION_PENDING) {
return Optional.of(pendingCommand.getMovementCommand());
}
return Optional.empty();
}
/**
* Returns the resources for which allocation is currently pending.
*
* @return An optional containing the resources for which allocation is currently pending or
* {@link Optional#empty()} if there are no such resources.
* @see #getAllocationPendingCommand()
*/
public Optional<Set<TCSResource<?>>> getAllocationPendingResources() {
if (pendingCommandState == PendingCommandState.ALLOCATION_PENDING) {
return Optional.of(pendingCommand.getResources());
}
return Optional.empty();
}
/**
* Returns the movement command for which resources have already been allocated but which is yet
* to be sent to the communication adapter.
*
* @return An optional containing the movement command for which resources have already been
* allocated but which is yet to be sent to the communication adapter or {@link Optional#empty()}
* if there is no such command.
*/
public Optional<MovementCommand> getSendingPendingCommand() {
if (pendingCommandState == PendingCommandState.SENDING_PENDING) {
return Optional.of(pendingCommand.getMovementCommand());
}
return Optional.empty();
}
/**
* Returns the queue of movement commands that have been sent to the communication adapter but
* have not yet been reported as executed.
*
* @return The queue of movement commands that have been sent to the communication adapter but
* have not yet been reported as executed.
*/
public Deque<MovementCommand> getSentCommands() {
return sentCommands.stream()
.map(CommandResourcePair::getMovementCommand)
.collect(Collectors.toCollection(ArrayDeque::new));
}
/**
* Returns the movement command that was executed last.
*
* @return The movement command that was executed last.
*/
public Optional<MovementCommand> getLastCommandExecuted() {
return Optional.ofNullable(lastCommandExecuted);
}
/**
* Returns the movement command for which resources are to be allocated next.
*
* @return The movement command for which resources are to be allocated next.
* @see #getNextAllocationResources()
*/
public Optional<MovementCommand> getNextAllocationCommand() {
return Optional.ofNullable(futureCommands.peek())
.map(CommandResourcePair::getMovementCommand);
}
/**
* Returns the resources that are to be allocated next.
*
* @return The resources that are to be allocated next.
* @see #getNextAllocationCommand()
*/
public Optional<Set<TCSResource<?>>> getNextAllocationResources() {
return Optional.ofNullable(futureCommands.peek())
.map(CommandResourcePair::getResources);
}
/**
* Checks if there are resources for which allocation was requested but is yet to be confirmed.
*
* @return {@code true} if there are resources for which allocation was requested but is yet to
* be confirmed, otherwise {@code false}.
*/
public boolean isWaitingForAllocation() {
return pendingCommandState == PendingCommandState.ALLOCATION_PENDING;
}
private SequencedCollection<CommandResourcePair> toCommandResourcePairs(
SequencedCollection<MovementCommand> movementCommands
) {
return movementCommands.stream()
.map(command -> new CommandResourcePair(command, getNeededResources(command)))
.toList();
}
private void discardProcessedFutureCommands() {
MovementCommand lastCommandProcessed = lastCommandProcessed();
if (futureCommands.isEmpty()) {
// There are no commands to be discarded.
return;
}
if (!fromSameDriveOrder(lastCommandProcessed, futureCommands.peek().getMovementCommand())) {
// If the last processed command is from a different drive order, there is nothing to be
// discarded. This is the case, for example, if the vehicle didn't yet process the very first
// movement command of a new drive order.
return;
}
LOG.debug(
"{}: Discarding future commands up to '{}' (inclusively): {}",
lastCommandProcessed.getTransportOrder().getProcessingVehicle().getName(),
lastCommandProcessed,
futureCommands
);
// Discard commands up to lastCommandProcessed...
while (!futureCommands.isEmpty()
&& !lastCommandProcessed.equalsInMovement(futureCommands.peek().getMovementCommand())) {
futureCommands.remove();
}
checkState(
!futureCommands.isEmpty(),
"%s: Future commands should not be empty.",
lastCommandProcessed.getTransportOrder().getProcessingVehicle().getName(),
lastCommandProcessed
);
checkState(
lastCommandProcessed.equalsInMovement(futureCommands.peek().getMovementCommand()),
"%s: Last command processed is not head of future commands: %s (futureCommands=%s)",
lastCommandProcessed.getTransportOrder().getProcessingVehicle().getName(),
lastCommandProcessed,
futureCommands
);
// ...and also discard lastCommandProcessed itself.
futureCommands.remove();
}
/**
* Returns the last movement command that has been processed in a way that is relevant in the
* context of rerouting.
* <p>
* Generally, a movement command is processed in multiple stages. It is:
* <ol>
* <li>Added to the <code>futureCommands</code> queue (when a transport order for the vehicle
* is set or updated).</li>
* <li>Removed from the <code>futureCommands</code> queue and set as the
* <code>pendingCommand</code> (when allocation for the resources needed for executing the next
* command has been requested).</li>
* <li>Unset as the <code>pendingCommand</code> and added to the
* <code>commandsSent</code> queue (when the command has been handed over to the vehicle driver).
* </li>
* <li>Removed from the <code>commandsSent</code> queue and set as the
* <code>lastCommandExecuted</code> (when the driver reports that the command has been executed)
* </li>
* </ol>
* </p>
* <p>
* The earliest stage a movement command can be in that is relevant in the context of rerouting is
* when it is set as the <code>pendingCommand</code> with the state
* {@link PendingCommandState#SENDING_PENDING}. At this stage, the resources for the command have
* already been (successfully) allocated, and it will either be handed over to the vehicle driver
* or discarded. Rerouting should therefore take place from this command (or rather the respective
* step) at the earliest.
* </p>
* <p>
* <code>pendingCommand</code> with the state {@link PendingCommandState#ALLOCATION_PENDING}
* (as well as everything prior to that) is not relevant here, as the allocation for corresponding
* resources is still pending at this stage, and all pending allocations are cleared upon
* rerouting.
* </p>
*
* @return A movement command or {@code null} if there is no movement command that has been
* processed by this vehicle controller in a way that is relevant in the context of rerouting.
*/
@Nullable
private MovementCommand lastCommandProcessed() {
return Optional.ofNullable(pendingCommand)
.or(() -> Optional.ofNullable(sentCommands.peekLast()))
.map(CommandResourcePair::getMovementCommand)
.orElse(lastCommandExecuted);
}
private boolean fromSameDriveOrder(
@Nullable
MovementCommand commandA,
@Nullable
MovementCommand commandB
) {
return commandA != null
&& commandB != null
&& Objects.equals(commandA.getDriveOrder(), commandB.getDriveOrder());
}
/**
* Returns a set of resources needed for executing the given command.
*
* @param cmd The command for which to return the needed resources.
* @return A set of resources needed for executing the given command.
*/
@Nonnull
private Set<TCSResource<?>> getNeededResources(MovementCommand cmd) {
requireNonNull(cmd, "cmd");
Set<TCSResource<?>> result = new HashSet<>();
result.add(cmd.getStep().getDestinationPoint());
if (cmd.getStep().getPath() != null) {
result.add(cmd.getStep().getPath());
}
if (cmd.getOpLocation() != null) {
result.add(cmd.getOpLocation());
}
return result;
}
private Set<TCSResource<?>> extractPointAndLocation(Set<TCSResource<?>> resources) {
return resources.stream()
.filter(resource -> resource instanceof Point || resource instanceof Location)
.collect(Collectors.toSet());
}
private Set<TCSResource<?>> extractPath(Set<TCSResource<?>> resources) {
return resources.stream()
.filter(resource -> resource instanceof Path)
.collect(Collectors.toSet());
}
/**
* Defines the states the pending command can be in.
*/
private enum PendingCommandState {
/**
* The state is undefined. This is the case when the pending command is {@code null}.
*/
UNDEFINED,
/**
* Allocation of the resources for the pending command was requested but is yet to be confirmed.
*/
ALLOCATION_PENDING,
/**
* Allocation of the resources for the pending command was confirmed but the command is yet to
* be sent to the communication adapter.
*/
SENDING_PENDING,
/**
* The pending command won't be sent to the communication adapter but the corresponding
* resources are still allocated.
*/
WONT_SEND;
}
/**
* A wrapper for a {@link MovementCommand} and the {@link TCSResource}s that are associated with
* it.
*/
private static class CommandResourcePair {
private final MovementCommand movementCommand;
private final Set<TCSResource<?>> resources;
/**
* Creates a new instance.
*
* @param movementCommand The movement command.
* @param resources The set of resources associated with the movement command.
*/
CommandResourcePair(MovementCommand movementCommand, Set<TCSResource<?>> resources) {
this.movementCommand = requireNonNull(movementCommand, "movementCommand");
this.resources = requireNonNull(resources, "resources");
}
/**
* Returns the movement command.
*
* @return The movement command.
*/
public MovementCommand getMovementCommand() {
return movementCommand;
}
/**
* Returns the set of resources associated with the movement command.
*
* @return The set of resources associated with the movement command.
*/
public Set<TCSResource<?>> getResources() {
return resources;
}
@Override
public String toString() {
return "CommandResourcePair{" +
"movementCommand=" + movementCommand +
", resources=" + resources +
'}';
}
}
}

View File

@@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import org.opentcs.components.kernel.services.InternalVehicleService;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.VehicleCommAdapter;
import org.opentcs.drivers.vehicle.VehicleController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Maintains associations of {@link Vehicle}, {@link VehicleController} and
* {@link VehicleCommAdapter}.
*/
public final class DefaultVehicleControllerPool
implements
LocalVehicleControllerPool {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(DefaultVehicleControllerPool.class);
/**
* The vehicle service.
*/
private final InternalVehicleService vehicleService;
/**
* A factory for vehicle managers.
*/
private final VehicleControllerFactory vehicleManagerFactory;
/**
* The currently existing/assigned managers, mapped by the names of the
* corresponding vehicles.
*/
private final Map<String, PoolEntry> poolEntries = new HashMap<>();
/**
* Indicates whether this components is initialized.
*/
private boolean initialized;
/**
* Creates a new StandardVehicleManagerPool.
*
* @param vehicleService The vehicle service.
* @param vehicleManagerFactory A factory for vehicle managers.
*/
@Inject
public DefaultVehicleControllerPool(
InternalVehicleService vehicleService,
VehicleControllerFactory vehicleManagerFactory
) {
this.vehicleService = requireNonNull(vehicleService, "vehicleService");
this.vehicleManagerFactory = requireNonNull(vehicleManagerFactory, "vehicleManagerFactory");
}
@Override
public void initialize() {
if (initialized) {
LOG.debug("Already initialized, doing nothing.");
return;
}
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!initialized) {
LOG.debug("Not initialized, doing nothing.");
return;
}
// Detach all vehicles and reset their positions.
for (PoolEntry curEntry : poolEntries.values()) {
curEntry.vehicleController.terminate();
Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, curEntry.vehicleName);
vehicleService.updateVehiclePosition(vehicle.getReference(), null);
}
poolEntries.clear();
initialized = false;
}
@Override
public synchronized void attachVehicleController(
String vehicleName,
VehicleCommAdapter commAdapter
) {
requireNonNull(vehicleName, "vehicleName");
requireNonNull(commAdapter, "commAdapter");
if (poolEntries.containsKey(vehicleName)) {
LOG.warn("manager already attached, doing nothing");
return;
}
Vehicle vehicle = vehicleService.fetchObject(Vehicle.class, vehicleName);
checkArgument(vehicle != null, "No such vehicle: %s", vehicleName);
VehicleController controller = vehicleManagerFactory.createVehicleController(
vehicle,
commAdapter
);
PoolEntry poolEntry = new PoolEntry(vehicleName, controller);
poolEntries.put(vehicleName, poolEntry);
controller.initialize();
}
@Override
public synchronized void detachVehicleController(String vehicleName) {
requireNonNull(vehicleName, "vehicleName");
LOG.debug("Detaching controller for vehicle {}...", vehicleName);
PoolEntry poolEntry = poolEntries.remove(vehicleName);
if (poolEntry == null) {
LOG.debug("A vehicle named '{}' is not attached to a controller.", vehicleName);
return;
}
// Clean up - mark vehicle state and adapter state as unknown.
poolEntry.vehicleController.terminate();
}
@Override
public VehicleController getVehicleController(String vehicleName) {
requireNonNull(vehicleName, "vehicleName");
PoolEntry poolEntry = poolEntries.get(vehicleName);
return poolEntry == null ? new NullVehicleController(vehicleName) : poolEntry.vehicleController;
}
/**
* An entry in this vehicle manager pool.
*/
private static final class PoolEntry {
/**
* The name of the vehicle managed.
*/
private final String vehicleName;
/**
* The vehicle controller associated with the vehicle.
*/
private final VehicleController vehicleController;
/**
* Creates a new pool entry.
*
* @param name The name of the vehicle managed.
* @param manager The vehicle manager associated with the vehicle.
* @param controller The VehicleController
*/
private PoolEntry(String name, VehicleController controller) {
vehicleName = requireNonNull(name, "name");
vehicleController = requireNonNull(controller, "controller");
}
}
}

View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import org.opentcs.components.Lifecycle;
import org.opentcs.drivers.vehicle.VehicleCommAdapter;
import org.opentcs.drivers.vehicle.VehicleControllerPool;
/**
* Manages the attachment of vehicle controllers to vehicles and comm adapters.
*/
public interface LocalVehicleControllerPool
extends
VehicleControllerPool,
Lifecycle {
/**
* Associates a vehicle controller with a named vehicle and a comm adapter.
*
* @param vehicleName The name of the vehicle.
* @param commAdapter The communication adapter that is going to control the physical vehicle.
*/
void attachVehicleController(String vehicleName, VehicleCommAdapter commAdapter)
throws IllegalArgumentException;
/**
* Disassociates a vehicle control and a comm adapter from a vehicle.
*
* @param vehicleName The name of the vehicle from which to detach.
*/
void detachVehicleController(String vehicleName);
}

View File

@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.opentcs.components.kernel.services.TCSObjectService;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.Point;
import org.opentcs.data.order.DriveOrder;
import org.opentcs.data.order.Route;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.drivers.vehicle.MovementCommand;
/**
* Provides methods for mapping {@link DriveOrder}s to a list of {@link MovementCommand}s.
*/
public class MovementCommandMapper {
private final TCSObjectService objectService;
/**
* Creates a new instance.
*
* @param objectService The object service to use.
*/
@Inject
public MovementCommandMapper(TCSObjectService objectService) {
this.objectService = requireNonNull(objectService, "objectService");
}
/**
* Maps the given {@link DriveOrder} to a corresponding list of {@link MovementCommand}s.
*
* @param driveOrder The {@link DriveOrder} to map.
* @param transportOrder The {@link TransportOrder} the drive order belongs to.
* @return A list of {@link MovementCommand}s.
*/
public List<MovementCommand> toMovementCommands(
DriveOrder driveOrder,
TransportOrder transportOrder
) {
requireNonNull(driveOrder, "driveOrder");
requireNonNull(transportOrder, "transportOrder");
// Gather information from the drive order.
String op = driveOrder.getDestination().getOperation();
Route orderRoute = driveOrder.getRoute();
Point finalDestination = orderRoute.getFinalDestinationPoint();
Location finalDestinationLocation
= objectService.fetchObject(
Location.class,
driveOrder.getDestination().getDestination().getName()
);
Map<String, String> destProperties = driveOrder.getDestination().getProperties();
List<MovementCommand> result = new ArrayList<>(orderRoute.getSteps().size());
// Create a movement command for every step in the drive order's route.
Iterator<Route.Step> stepIter = orderRoute.getSteps().iterator();
while (stepIter.hasNext()) {
Route.Step curStep = stepIter.next();
boolean isFinalMovement = !stepIter.hasNext();
String operation = isFinalMovement ? op : MovementCommand.NO_OPERATION;
Location location = isFinalMovement ? finalDestinationLocation : null;
result.add(
new MovementCommand(
transportOrder,
driveOrder,
curStep,
operation,
location,
isFinalMovement,
finalDestinationLocation,
finalDestination,
op,
mergeProperties(transportOrder.getProperties(), destProperties)
)
);
}
return result;
}
/**
* Merges the properties of a transport order and those of a drive order.
*
* @param orderProps The properties of a transport order.
* @param destProps The properties of a drive order destination.
* @return The merged properties.
*/
private Map<String, String> mergeProperties(
Map<String, String> orderProps,
Map<String, String> destProps
) {
requireNonNull(orderProps, "orderProps");
requireNonNull(destProps, "destProps");
Map<String, String> result = new HashMap<>();
result.putAll(orderProps);
result.putAll(destProps);
return result;
}
}

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static java.util.Objects.requireNonNull;
import java.util.Iterator;
import java.util.List;
import org.opentcs.data.order.Route.Step;
import org.opentcs.drivers.vehicle.MovementCommand;
/**
* Provides methods for comparing movements represented by {@link MovementCommand}s and
* {@link Step}s.
*/
public class MovementComparisons {
private MovementComparisons() {
}
/**
* Compares the two given lists of steps, ignoring rerouting-related properties.
*
* @param stepsA The first list of steps.
* @param stepsB The second list of steps.
* @return {@code true}, if the given lists of steps are equal (ignoring rerouting-related
* properties), otherwise {@code false}.
*/
public static boolean equalsInMovement(List<Step> stepsA, List<Step> stepsB) {
requireNonNull(stepsA, "stepsA");
requireNonNull(stepsB, "stepsB");
if (stepsA.size() != stepsB.size()) {
return false;
}
Iterator<Step> itStepsA = stepsA.iterator();
Iterator<Step> itStepsB = stepsB.iterator();
while (itStepsA.hasNext()) {
if (!itStepsA.next().equalsInMovement(itStepsB.next())) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import java.util.ArrayDeque;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import org.opentcs.data.model.TCSResource;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.drivers.vehicle.AdapterCommand;
import org.opentcs.drivers.vehicle.MovementCommand;
import org.opentcs.drivers.vehicle.VehicleController;
import org.opentcs.util.ExplainedBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Null-object implementation of {@link VehicleController}.
*/
public class NullVehicleController
implements
VehicleController {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(NullVehicleController.class);
/**
* The associated vehicle's name.
*/
private final String vehicleName;
/**
* Creates a new instance.
*
* @param vehicleName The associated vehicle's name.
*/
public NullVehicleController(
@Nonnull
String vehicleName
) {
this.vehicleName = requireNonNull(vehicleName, "vehicleName");
}
@Override
public void initialize() {
}
@Override
public boolean isInitialized() {
return true;
}
@Override
public void terminate() {
}
@Override
public void setTransportOrder(TransportOrder newOrder) {
LOG.warn("No comm adapter attached to vehicle {}", vehicleName);
}
@Override
public void abortTransportOrder(boolean immediate) {
LOG.warn("No comm adapter attached to vehicle {}", vehicleName);
}
@Override
public ExplainedBoolean canProcess(TransportOrder order) {
return new ExplainedBoolean(false, "NullVehicleController");
}
@Override
public void sendCommAdapterMessage(Object message) {
LOG.warn("No comm adapter attached to vehicle {}", vehicleName);
}
@Override
public void sendCommAdapterCommand(AdapterCommand command) {
LOG.warn("No comm adapter attached to vehicle {}", vehicleName);
}
@Override
public Queue<MovementCommand> getCommandsSent() {
LOG.warn("No comm adapter attached to vehicle {}", vehicleName);
return new ArrayDeque<>();
}
@Override
public void onVehiclePaused(boolean paused) {
}
@Override
public Optional<MovementCommand> getInteractionsPendingCommand() {
return Optional.empty();
}
@Override
public boolean mayAllocateNow(Set<TCSResource<?>> resources) {
return false;
}
}

View File

@@ -0,0 +1,347 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkState;
import jakarta.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.opentcs.access.to.peripherals.PeripheralJobCreationTO;
import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO;
import org.opentcs.components.kernel.services.PeripheralJobService;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.data.peripherals.PeripheralJob;
import org.opentcs.data.peripherals.PeripheralOperation;
import org.opentcs.drivers.vehicle.MovementCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Describes an interaction with any number of peripheral devices.
* <p>
* An interaction is always associated with a {@link MovementCommand} and contains
* {@link PeripheralOperation}s for which {@link PeripheralJob}s are created once the interaction
* is started.
* </p>
* <p>
* In case there are no operations with the completion required flag set, the interaction is marked
* as finished immediately after it was started.
* In case there are operations with the completion required flag set, the interaction is only
* marked as finished after all corresponding jobs have been processed by the respective peripheral
* device.
* </p>
* <p>
* Once the interaction is finished, an interaction-specific callback is executed.
* </p>
*/
public class PeripheralInteraction {
/**
* This class's logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(PeripheralInteraction.class);
/**
* The reference to the vehicle that's interacting with peripheral devices.
*/
private final TCSObjectReference<Vehicle> vehicleRef;
/**
* A reference to the transport order this interaction is related to.
*/
private final TCSObjectReference<TransportOrder> orderRef;
/**
* The movement command this interaction is associated with.
*/
private final MovementCommand movementCommand;
/**
* The operations that jobs have to be created for.
*/
private final List<PeripheralOperation> operations;
/**
* The jobs that are required to be finished in order for this interaction itself to be marked as
* finished.
*/
private final List<PeripheralJob> pendingJobsWithCompletionRequired = new ArrayList<>();
/**
* The peripheral job service to use for creating jobs.
*/
private final PeripheralJobService peripheralJobService;
/**
* The reservation token to use for creating jobs.
*/
private final String reservationToken;
/**
* The callback that is to be executed if the interaction succeeds.
*/
private Runnable interactionSucceededCallback;
/**
* The callback that is executed if the interaction fails.
*/
private Runnable interactionFailedCallback;
/**
* The state of the interaction.
*/
private State state = State.PRISTINE;
/**
* Creates a new instance.
*
* @param vehicleRef The reference to the vehicle that's interacting with peripheral devices.
* @param orderRef A reference to the transport order that this interaction is related to.
* @param movementCommand The movement command this interaction is associated with.
* @param operations The operations that jobs have to be created for.
* @param peripheralJobService The peripheral job service to use for creating jobs.
* @param reservationToken The reservation token to use for creating jobs.
*/
public PeripheralInteraction(
@Nonnull
TCSObjectReference<Vehicle> vehicleRef,
@Nonnull
TCSObjectReference<TransportOrder> orderRef,
@Nonnull
MovementCommand movementCommand,
@Nonnull
List<PeripheralOperation> operations,
@Nonnull
PeripheralJobService peripheralJobService,
@Nonnull
String reservationToken
) {
this.vehicleRef = requireNonNull(vehicleRef, "vehicleRef");
this.orderRef = requireNonNull(orderRef, "orderRef");
this.movementCommand = requireNonNull(movementCommand, "movementCommand");
this.operations = requireNonNull(operations, "operations");
this.peripheralJobService = requireNonNull(peripheralJobService, "peripheralJobService");
this.reservationToken = requireNonNull(reservationToken, "reservationToken");
}
/**
* Starts this peripheral interaction.
*
* @param interactionSucceededCallback The callback that is to be executed if the interaction
* succeeds.
* @param interactionFailedCallback The callback that is to be executed if the interaction fails.
*/
public void start(
@Nonnull
Runnable interactionSucceededCallback,
@Nonnull
Runnable interactionFailedCallback
) {
this.interactionSucceededCallback = requireNonNull(
interactionSucceededCallback,
"interactionSucceededCallback"
);
this.interactionFailedCallback = requireNonNull(
interactionFailedCallback,
"interactionFailedCallback"
);
LOG.debug(
"{}: Starting peripheral interaction for movement to {}",
vehicleRef.getName(),
movementCommand.getStep().getDestinationPoint().getName()
);
for (PeripheralOperation operation : operations) {
PeripheralJob job = createPeripheralJob(operation);
if (operation.isCompletionRequired()) {
pendingJobsWithCompletionRequired.add(job);
}
}
state = State.STARTED;
if (pendingJobsWithCompletionRequired.isEmpty()) {
onInteractionFinished();
}
}
/**
* Informs this interaction that a peripheral job (that might be of interest for this interaction)
* has been finished.
*
* @param job The peripheral job that has been finished.
*/
public void onPeripheralJobFinished(
@Nonnull
PeripheralJob job
) {
requireNonNull(job, "job");
if (pendingJobsWithCompletionRequired.remove(job)
&& pendingJobsWithCompletionRequired.isEmpty()) {
// The last pending job has been finished.
onInteractionFinished();
}
}
/**
* Informs this interaction that a peripheral job (that might be of interest for this interaction)
* has failed.
*
* @param job The peripheral job that has failed.
*/
public void onPeripheralJobFailed(
@Nonnull
PeripheralJob job
) {
requireNonNull(job, "job");
if (pendingJobsWithCompletionRequired.contains(job)) {
// As soon as one of the jobs for this interaction fails, the entire interaction itself is
// considered failed. At this point, we're no longer interested in any of the pending jobs and
// don't expect any other job to be reported as finished or failed. Therefore, simply forget
// all pending jobs and mark the interaction as failed. This also ensures that the callback
// for the failed interaction is called only once.
pendingJobsWithCompletionRequired.clear();
onInteractionFailed();
}
}
/**
* Returns the movement command this interaction is associated with.
*
* @return The movement command this interaction is associated with.
*/
public MovementCommand getMovementCommand() {
return movementCommand;
}
/**
* Returns whether the interaction is finished.
*
* @return Whether the interaction is finished.
*/
public boolean isFinished() {
return hasState(State.FINISHED);
}
/**
* Returns whether the interaction has failed.
*
* @return Whether the interaction has failed.
*/
public boolean isFailed() {
return hasState(State.FAILED);
}
/**
* Returns whether the interaction is in the given state.
*
* @param state The state.
* @return Whether the interaction is in the given state.
*/
public boolean hasState(State state) {
return this.state == state;
}
/**
* Returns whether this interaction has some operations that are required to be completed.
*
* @return Whether this interaction has some operations that are required to be completed.
*/
public boolean hasRequiredOperations() {
return operations.stream()
.anyMatch(PeripheralOperation::isCompletionRequired);
}
/**
* Returns the list of operations that are required to be completed and that haven't been
* completed yet.
*
* @return A list of operations.
*/
public List<PeripheralOperation> getPendingRequiredOperations() {
// If we're already done interacting with the peripheral device, there cannot be any pending
// operations.
if (hasState(State.FINISHED)) {
return new ArrayList<>();
}
if (!hasRequiredOperations()) {
return new ArrayList<>();
}
if (!pendingJobsWithCompletionRequired.isEmpty()) {
// The interaction is still ongoing. Jobs are not yet finished or have even failed.
return pendingJobsWithCompletionRequired.stream()
.map(job -> job.getPeripheralOperation())
.collect(Collectors.toList());
}
// The interaction is still ongoing but no jobs have been created (yet) for the required
// operations.
return operations.stream()
.filter(PeripheralOperation::isCompletionRequired)
.collect(Collectors.toList());
}
private void onInteractionFinished() {
checkState(interactionSucceededCallback != null, "The interaction hasn't been started yet.");
checkState(!hasState(State.FAILED), "The interaction has already been marked as failed.");
checkState(!hasState(State.FINISHED), "The interaction has already been marked as finished.");
state = State.FINISHED;
LOG.debug(
"{}: Peripheral interaction finished for movement to {}",
vehicleRef.getName(),
movementCommand.getStep().getDestinationPoint().getName()
);
interactionSucceededCallback.run();
}
private PeripheralJob createPeripheralJob(PeripheralOperation operation) {
return peripheralJobService.createPeripheralJob(
new PeripheralJobCreationTO(
"Job-",
reservationToken,
new PeripheralOperationCreationTO(
operation.getOperation(),
operation.getLocation().getName()
)
.withExecutionTrigger(operation.getExecutionTrigger())
.withCompletionRequired(operation.isCompletionRequired())
)
.withIncompleteName(true)
.withRelatedVehicleName(vehicleRef.getName())
.withRelatedTransportOrderName(orderRef.getName())
);
}
private void onInteractionFailed() {
checkState(interactionFailedCallback != null, "The interaction hasn't been started yet.");
checkState(!hasState(State.FINISHED), "The interaction has already been marked as finished.");
checkState(!hasState(State.FAILED), "The interaction has already been marked as failed.");
state = State.FAILED;
LOG.debug(
"{}: Peripheral interaction failed for movement to {}",
vehicleRef.getName(),
movementCommand.getStep().getDestinationPoint().getName()
);
interactionFailedCallback.run();
}
public enum State {
/**
* The interaction is initialized and yet to be started.
*/
PRISTINE,
/**
* The interaction was started.
*/
STARTED,
/**
* The interaction was finished.
* All the required operations (if any) have been finished successfully.
*/
FINISHED,
/**
* The interaction has failed.
* At least one of the required operations failed.
*/
FAILED;
}
}

View File

@@ -0,0 +1,461 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static java.util.Objects.requireNonNull;
import com.google.inject.assistedinject.Assisted;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.opentcs.components.Lifecycle;
import org.opentcs.components.kernel.services.PeripheralDispatcherService;
import org.opentcs.components.kernel.services.PeripheralJobService;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.TCSObject;
import org.opentcs.data.TCSObjectEvent;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Path;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.data.peripherals.PeripheralJob;
import org.opentcs.data.peripherals.PeripheralOperation;
import org.opentcs.drivers.vehicle.MovementCommand;
import org.opentcs.util.event.EventHandler;
import org.opentcs.util.event.EventSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages interactions with peripheral devices that are to be performed before or after the
* execution of movement commands.
*/
public class PeripheralInteractor
implements
EventHandler,
Lifecycle {
/**
* This class's logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(PeripheralInteractor.class);
/**
* The reference to the vehicle that's interacting with peripheral devices.
*/
private final TCSObjectReference<Vehicle> vehicleRef;
/**
* The peripheral job service to use.
*/
private final PeripheralJobService peripheralJobService;
/**
* The peripheral dispatcher service to use.
*/
private final PeripheralDispatcherService peripheralDispatcherService;
/**
* The event source to register with.
*/
private final EventSource eventSource;
/**
* The peripheral interactions to be performed BEFORE the execution of a movement command mapped
* to the corresponding movement command.
*/
private final Map<MovementCommand, PeripheralInteraction> preMovementInteractions
= new HashMap<>();
/**
* The peripheral interactions to be performed AFTER the execution of a movement command mapped
* to the corresponding movement command.
*/
private final Map<MovementCommand, PeripheralInteraction> postMovementInteractions
= new HashMap<>();
/**
* Indicates whether this instance is initialized.
*/
private boolean initialized;
/**
* Creates a new instance.
*
* @param vehicleRef The reference to the vehicle that's interacting with peripheral devices.
* @param peripheralJobService The peripheral job service to use.
* @param peripheralDispatcherService The peripheral dispatcher service to use.
* @param eventSource The event source to register with.
*/
@Inject
public PeripheralInteractor(
@Assisted
@Nonnull
TCSObjectReference<Vehicle> vehicleRef,
@Nonnull
PeripheralJobService peripheralJobService,
@Nonnull
PeripheralDispatcherService peripheralDispatcherService,
@Nonnull
@ApplicationEventBus
EventSource eventSource
) {
this.vehicleRef = requireNonNull(vehicleRef, "vehicleRef");
this.peripheralJobService = requireNonNull(peripheralJobService, "peripheralJobService");
this.peripheralDispatcherService = requireNonNull(
peripheralDispatcherService,
"peripheralDispatcherService"
);
this.eventSource = requireNonNull(eventSource, "eventSource");
}
@Override
public void initialize() {
if (isInitialized()) {
return;
}
eventSource.subscribe(this);
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
return;
}
eventSource.unsubscribe(this);
initialized = false;
}
@Override
public void onEvent(Object event) {
if (!(event instanceof TCSObjectEvent)) {
return;
}
TCSObjectEvent objectEvent = (TCSObjectEvent) event;
if (objectEvent.getType() != TCSObjectEvent.Type.OBJECT_MODIFIED) {
return;
}
TCSObject<?> currentOrPreviousObjectState = objectEvent.getCurrentOrPreviousObjectState();
if (!(currentOrPreviousObjectState instanceof PeripheralJob)) {
return;
}
// Since a PeripheralInteraction only keeps track of peripheral jobs where the completion
// required flag is set, we can ignore all peripheral jobs where this is not the case.
if (!hasCompletionRequiredFlagSet((PeripheralJob) currentOrPreviousObjectState)) {
return;
}
onPeripheralJobChange(objectEvent);
}
/**
* Prepares for peripheral interactions in the context of the given movement command by
* determining the interactions that have to be performed before and after the movement command
* is executed.
*
* @param orderRef A reference to the transport order that the movement command belongs to.
* @param movementCommand The movement command to prepare peripheral interactions for.
*/
public void prepareInteractions(
TCSObjectReference<TransportOrder> orderRef,
MovementCommand movementCommand
) {
Path path = movementCommand.getStep().getPath();
if (path == null) {
return;
}
Map<PeripheralOperation.ExecutionTrigger, List<PeripheralOperation>> operations
= path.getPeripheralOperations().stream()
.collect(Collectors.groupingBy(t -> t.getExecutionTrigger()));
operations.computeIfAbsent(
PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION,
executionTrigger -> new ArrayList<>()
);
operations.computeIfAbsent(
PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT,
executionTrigger -> new ArrayList<>()
);
String reservationToken = determineReservationToken();
List<PeripheralOperation> preMovementOperations
= operations.get(PeripheralOperation.ExecutionTrigger.AFTER_ALLOCATION);
if (!preMovementOperations.isEmpty()) {
preMovementInteractions.put(
movementCommand,
new PeripheralInteraction(
vehicleRef,
orderRef,
movementCommand,
preMovementOperations,
peripheralJobService,
reservationToken
)
);
}
List<PeripheralOperation> postMovementOperations
= operations.get(PeripheralOperation.ExecutionTrigger.AFTER_MOVEMENT);
if (!postMovementOperations.isEmpty()) {
postMovementInteractions.put(
movementCommand,
new PeripheralInteraction(
vehicleRef,
orderRef,
movementCommand,
postMovementOperations,
peripheralJobService,
reservationToken
)
);
}
}
/**
* Starts the peripheral interactions that have to be performed before the given movement command
* is executed.
*
* @param movementCommand The movement command.
* @param succeededCallback The callback that is executed if the interactions succeeds (i.e. once
* all required interactions are finished).
* @param failedCallback The callback that is executed if the interactions fails (i.e. if a
* single interaction failed).
*/
public void startPreMovementInteractions(
@Nonnull
MovementCommand movementCommand,
@Nonnull
Runnable succeededCallback,
@Nonnull
Runnable failedCallback
) {
requireNonNull(movementCommand, "movementCommand");
requireNonNull(succeededCallback, "succeededCallback");
requireNonNull(failedCallback, "failedCallback");
if (!preMovementInteractions.containsKey(movementCommand)) {
LOG.debug(
"{}: No interactions to be performed before movement to {}...",
vehicleRef.getName(),
movementCommand.getStep().getDestinationPoint().getName()
);
succeededCallback.run();
return;
}
LOG.debug(
"{}: There are interactions to be performed before movement to {}...",
vehicleRef.getName(),
movementCommand.getStep().getDestinationPoint().getName()
);
preMovementInteractions.get(movementCommand).start(succeededCallback, failedCallback);
// In case there are only operations with the completion required flag not set, the interaction
// is immediately finished and we can remove it right away.
if (preMovementInteractions.get(movementCommand).isFinished()) {
preMovementInteractions.remove(movementCommand);
}
// Peripheral jobs have been created. Dispatch them.
peripheralDispatcherService.dispatch();
}
/**
* Starts the peripheral interactions that have to be performed after the given movement command
* is executed.
*
* @param movementCommand The movement command.
* @param succeededCallback The callback that is executed if the interactions succeeds (i.e. once
* all required interactions are finished).
* @param failedCallback The callback that is executed if the interactions fails (i.e. if a
* single interaction failed).
*/
public void startPostMovementInteractions(
@Nonnull
MovementCommand movementCommand,
@Nonnull
Runnable succeededCallback,
@Nonnull
Runnable failedCallback
) {
requireNonNull(movementCommand, "movementCommand");
requireNonNull(succeededCallback, "succeededCallback");
requireNonNull(failedCallback, "failedCallback");
if (!postMovementInteractions.containsKey(movementCommand)) {
LOG.debug(
"{}: No interactions to be performed after movement to {}...",
vehicleRef.getName(),
movementCommand.getStep().getDestinationPoint().getName()
);
succeededCallback.run();
return;
}
LOG.debug(
"{}: There are interactions to be performed after movement to {}...",
vehicleRef.getName(),
movementCommand.getStep().getDestinationPoint().getName()
);
postMovementInteractions.get(movementCommand).start(succeededCallback, failedCallback);
// In case there are only operations with the completion required flag not set, the interaction
// is immediately finished and we can remove it right away.
if (postMovementInteractions.get(movementCommand).isFinished()) {
postMovementInteractions.remove(movementCommand);
}
// Peripheral jobs have been created. Dispatch them.
peripheralDispatcherService.dispatch();
}
/**
* Returns whether there are any required (pre or post movement) interactions that have not been
* finished yet.
* In case there's a required interaction that has failed (and therefore not finished), this
* method returns {@code true}.
*
* @return Whether there are any required interactions that have not been finished yet.
*/
public boolean isWaitingForMovementInteractionsToFinish() {
return isWaitingForPreMovementInteractionsToFinish()
|| isWaitingForPostMovementInteractionsToFinish();
}
/**
* Returns whether there are any required (pre movement) interactions that have not been finished
* yet.
* In case there's a required interaction that has failed (and therefore not finished), this
* method returns {@code true}.
*
* @return Whether there are any required (pre movement) interactions that have not been finished
* yet.
*/
public boolean isWaitingForPreMovementInteractionsToFinish() {
return !preMovementInteractions.values().stream()
.filter(PeripheralInteraction::hasRequiredOperations)
.allMatch(PeripheralInteraction::isFinished);
}
/**
* Returns whether there are any required (post movement) interactions that have not been finished
* yet.
* In case there's a required interaction that has failed (and therefore not finished), this
* method returns {@code true}.
*
* @return Whether there are any required (post movement) interactions that have not been finished
* yet.
*/
public boolean isWaitingForPostMovementInteractionsToFinish() {
return !postMovementInteractions.values().stream()
.filter(PeripheralInteraction::hasRequiredOperations)
.allMatch(PeripheralInteraction::isFinished);
}
/**
* Returns a list of required operations that are still to be completed mapped to the associated
* movement command's destination point.
*
* @return A list of required operations that are still to be completed mapped to the associated
* movement command's destination point.
*/
public Map<String, List<PeripheralOperation>> pendingRequiredInteractionsByDestination() {
return Stream.concat(
preMovementInteractions.entrySet().stream(),
postMovementInteractions.entrySet().stream()
)
.map(entry -> entry.getValue())
.filter(interaction -> interaction.hasRequiredOperations())
// We're working with two streams from two maps which can each contain the same keys.
// Therefore we have to use the groupingBy collector and need to flat map each interaction's
// pending required operations.
.collect(
Collectors.groupingBy(
interact -> interact.getMovementCommand().getStep().getDestinationPoint().getName(),
Collectors.flatMapping(
interaction -> interaction.getPendingRequiredOperations().stream(),
Collectors.toList()
)
)
);
}
private void onPeripheralJobChange(TCSObjectEvent event) {
PeripheralJob prevJobState = (PeripheralJob) event.getPreviousObjectState();
PeripheralJob currJobState = (PeripheralJob) event.getCurrentObjectState();
if (prevJobState.getState() != currJobState.getState()) {
switch (currJobState.getState()) {
case FINISHED:
onPeripheralJobFinished(currJobState);
break;
case FAILED:
onPeripheralJobFailed(currJobState);
break;
default: // Do nothing
}
}
}
private void onPeripheralJobFinished(PeripheralJob job) {
Stream.concat(
preMovementInteractions.values().stream(),
postMovementInteractions.values().stream()
)
.forEach(interaction -> interaction.onPeripheralJobFinished(job));
// With a peripheral job finished, an associated interaction might now be finished as well. If
// that's the case, forget the interaction.
preMovementInteractions.entrySet().removeIf(entry -> entry.getValue().isFinished());
postMovementInteractions.entrySet().removeIf(entry -> entry.getValue().isFinished());
}
private void onPeripheralJobFailed(PeripheralJob job) {
Stream.concat(
preMovementInteractions.values().stream(),
postMovementInteractions.values().stream()
)
.forEach(interaction -> interaction.onPeripheralJobFailed(job));
// With a peripheral job failed, an associated interaction might now be failed as well. If
// that's the case, forget the interaction.
preMovementInteractions.entrySet().removeIf(entry -> entry.getValue().isFailed());
postMovementInteractions.entrySet().removeIf(entry -> entry.getValue().isFailed());
}
/**
* Clears the interactions.
*/
public void clear() {
preMovementInteractions.clear();
postMovementInteractions.clear();
}
private String determineReservationToken() {
Vehicle vehicle = peripheralJobService.fetchObject(Vehicle.class, vehicleRef);
if (vehicle.getTransportOrder() != null) {
TransportOrder transportOrder = peripheralJobService.fetchObject(
TransportOrder.class,
vehicle.getTransportOrder()
);
if (transportOrder.getPeripheralReservationToken() != null) {
return transportOrder.getPeripheralReservationToken();
}
}
return vehicle.getName();
}
private boolean hasCompletionRequiredFlagSet(PeripheralJob job) {
return job.getPeripheralOperation().isCompletionRequired();
}
}

View File

@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.opentcs.data.model.Path;
import org.opentcs.data.model.TCSResource;
/**
* Utility methods for resource-related computations.
*/
public class ResourceMath {
/**
* Prevents instantiation.
*/
private ResourceMath() {
}
/**
* Returns the number of resource sets that could be freed based on the given vehicle length.
*
* @param resourcesPassed A list of passed resource sets still allocated for a vehicle, from
* oldest to youngest, including those for the vehicle's current position (= the last set in the
* list).
* @param vehicleLength The vehicle's length. Must be a positive value.
* @return The number of resource sets from {@code resourcesPassed} that could be freed because
* they are not covered by the vehicle's length any more.
*/
public static int freeableResourceSetCount(
@Nonnull
List<Set<TCSResource<?>>> resourcesPassed,
long vehicleLength
) {
requireNonNull(resourcesPassed, "resourcesPassed");
checkArgument(vehicleLength > 0, "vehicleLength <= 0");
// We want to iterate over the passed resources from youngest (= current position) to oldest, so
// we reverse the list here.
List<Set<TCSResource<?>>> reversedPassedResources = new ArrayList<>(resourcesPassed);
Collections.reverse(reversedPassedResources);
long remainingRequiredLength = vehicleLength;
int result = 0;
for (Set<TCSResource<?>> curSet : reversedPassedResources) {
if (remainingRequiredLength > 0) {
remainingRequiredLength -= requiredLength(curSet);
}
else {
result++;
}
}
return result;
}
private static long requiredLength(Set<TCSResource<?>> resources) {
return resources.stream()
.filter(resource -> resource instanceof Path)
.mapToLong(resource -> ((Path) resource).getLength())
.sum();
}
}

View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.opentcs.data.model.TCSResource;
/**
* A vehicle's resources, split into resources the vehicle has already passed (including the
* resources for the vehicle's current position) and resources that still lay ahead of it.
*/
public class SplitResources {
private final List<Set<TCSResource<?>>> resourcesPassed;
private final List<Set<TCSResource<?>>> resourcesAhead;
/**
* Creates a new instance.
*
* @param resourcesPassed The passed resources, including the ones for the vehicle's current
* position.
* @param resourcesAhead The resources ahead of the vehicle.
*/
public SplitResources(
@Nonnull
List<Set<TCSResource<?>>> resourcesPassed,
@Nonnull
List<Set<TCSResource<?>>> resourcesAhead
) {
this.resourcesPassed = requireNonNull(resourcesPassed, "resourcesPassed");
this.resourcesAhead = requireNonNull(resourcesAhead, "resourcesAhead");
}
/**
* Returns the resources the vehicle has already passed, from oldest to youngest, with the
* youngest being the resources for the vehicle's current position.
*
* @return The resources the vehicle has already passed, from oldest to youngest, with the
* youngest being the resources for the vehicle's current position.
*/
public List<Set<TCSResource<?>>> getResourcesPassed() {
return resourcesPassed;
}
/**
* Returns the resources ahead of the vehicle, from oldest to youngest.
*
* @return The resources ahead of the vehicle, from oldest to youngest.
*/
public List<Set<TCSResource<?>>> getResourcesAhead() {
return resourcesAhead;
}
/**
* Returns a new instance created from the given iterable of resources, split at the element that
* contains the given delimiter (resources).
*
* @param resourceSets The iterable of resources to be split, from oldest to youngest.
* @param delimiter The delimiter / resources for the vehicle's current position.
* @return A new instance created from the given iterable of resources, split at the element that
* contains the given delimiter (resources).
*/
public static SplitResources from(
@Nonnull
Iterable<Set<TCSResource<?>>> resourceSets,
@Nonnull
Set<TCSResource<?>> delimiter
) {
requireNonNull(resourceSets, "resourceSets");
requireNonNull(delimiter, "delimiter");
List<Set<TCSResource<?>>> resourcesPassed = new ArrayList<>();
List<Set<TCSResource<?>>> resourcesAhead = new ArrayList<>();
List<Set<TCSResource<?>>> resourcesToPutIn = resourcesPassed;
for (Set<TCSResource<?>> curSet : resourceSets) {
resourcesToPutIn.add(curSet);
if (!delimiter.isEmpty() && curSet.containsAll(delimiter)) {
resourcesToPutIn = resourcesAhead;
}
}
return new SplitResources(resourcesPassed, resourcesAhead);
}
}

View File

@@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.opentcs.access.LocalKernel;
import org.opentcs.components.Lifecycle;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.VehicleCommAdapterDescription;
import org.opentcs.drivers.vehicle.VehicleCommAdapterFactory;
import org.opentcs.virtualvehicle.LoopbackCommunicationAdapterDescription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A registry for all communication adapters in the system.
*/
public class VehicleCommAdapterRegistry
implements
Lifecycle {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(VehicleCommAdapterRegistry.class);
/**
* The registered factories. Uses a comparator to sort the loopback driver to the end.
*/
private final Map<VehicleCommAdapterDescription, VehicleCommAdapterFactory> factories
= new TreeMap<>((f1, f2) -> {
if (f1 instanceof LoopbackCommunicationAdapterDescription
&& f2 instanceof LoopbackCommunicationAdapterDescription) {
return 0;
}
if (f1 instanceof LoopbackCommunicationAdapterDescription) {
return 1;
}
else if (f2 instanceof LoopbackCommunicationAdapterDescription) {
return -1;
}
return f1.getDescription().compareTo(f2.getDescription());
});
/**
* Indicates whether this component is initialized or not.
*/
private boolean initialized;
/**
* Creates a new registry.
*
* @param kernel A reference to the local kernel.
* @param factories The comm adapter factories.
*/
@Inject
public VehicleCommAdapterRegistry(LocalKernel kernel, Set<VehicleCommAdapterFactory> factories) {
requireNonNull(kernel, "kernel");
for (VehicleCommAdapterFactory factory : factories) {
LOG.info("Setting up communication adapter factory: {}", factory.getClass().getName());
this.factories.put(factory.getDescription(), factory);
}
checkState(!factories.isEmpty(), "No adapter factories found.");
}
@Override
public void initialize() {
if (initialized) {
LOG.debug("Already initialized.");
return;
}
for (VehicleCommAdapterFactory factory : factories.values()) {
factory.initialize();
}
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!initialized) {
LOG.debug("Not initialized.");
return;
}
for (VehicleCommAdapterFactory factory : factories.values()) {
factory.terminate();
}
initialized = false;
}
/**
* Returns all registered factories that can provide communication adapters.
*
* @return All registered factories that can provide communication adapters.
*/
public List<VehicleCommAdapterFactory> getFactories() {
return new ArrayList<>(factories.values());
}
/**
* Returns the factory for the given description.
*
* @param description The description to get the factory for.
* @return The factory for the given description.
*/
public VehicleCommAdapterFactory findFactoryFor(VehicleCommAdapterDescription description) {
requireNonNull(description, "description");
checkArgument(
factories.get(description) != null,
"No factory for description %s",
description
);
return factories.get(description);
}
/**
* Returns a set of factories that can provide communication adapters for the
* given vehicle.
*
* @param vehicle The vehicle to find communication adapters/factories for.
* @return A set of factories that can provide communication adapters for the
* given vehicle.
*/
public List<VehicleCommAdapterFactory> findFactoriesFor(Vehicle vehicle) {
requireNonNull(vehicle, "vehicle");
return factories.values().stream()
.filter(factory -> factory.providesAdapterFor(vehicle))
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Vehicle;
/**
* A factory for various components related to a vehicle controller.
*/
public interface VehicleControllerComponentsFactory {
/**
* Creates a new {@link PeripheralInteractor} instance for the given vehicle.
*
* @param vehicleRef The vehicle.
* @return A new peripheral interactor.
*/
PeripheralInteractor createPeripheralInteractor(TCSObjectReference<Vehicle> vehicleRef);
}

View File

@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.VehicleCommAdapter;
/**
* A factory for <code>VehicleManager</code> instances.
*/
public interface VehicleControllerFactory {
/**
* Creates a new vehicle controller for the given vehicle and communication adapter.
*
* @param vehicle The vehicle.
* @param commAdapter The communication adapter.
* @return A new vehicle controller.
*/
DefaultVehicleController createVehicleController(Vehicle vehicle, VehicleCommAdapter commAdapter);
}

View File

@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles.transformers;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import java.util.Optional;
import org.opentcs.data.model.Pose;
import org.opentcs.data.model.Triple;
import org.opentcs.drivers.vehicle.IncomingPoseTransformer;
/**
* Transforms {@link Pose}s by subtracting offsets in a given
* {@link CoordinateSystemTransformation}.
*/
public class CoordinateSystemIncomingPoseTransformer
implements
IncomingPoseTransformer {
private final CoordinateSystemTransformation transformation;
public CoordinateSystemIncomingPoseTransformer(
@Nonnull
CoordinateSystemTransformation transformation
) {
this.transformation = requireNonNull(transformation, "transformation");
}
@Override
public Pose apply(
@Nonnull
Pose pose
) {
requireNonNull(pose, "pose");
return pose
.withPosition(transformTriple(pose.getPosition()))
.withOrientationAngle(
(pose.getOrientationAngle() - transformation.getOffsetOrientation()) % 360.0
);
}
private Triple transformTriple(Triple triple) {
return Optional.ofNullable(triple)
.map(
originalTriple -> new Triple(
originalTriple.getX() - transformation.getOffsetX(),
originalTriple.getY() - transformation.getOffsetY(),
originalTriple.getZ() - transformation.getOffsetZ()
)
)
.orElse(null);
}
}

View File

@@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles.transformers;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.Point;
import org.opentcs.data.model.Pose;
import org.opentcs.data.model.Triple;
import org.opentcs.data.order.DriveOrder;
import org.opentcs.data.order.Route;
import org.opentcs.data.order.Route.Step;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.drivers.vehicle.MovementCommand;
import org.opentcs.drivers.vehicle.MovementCommandTransformer;
/**
* Transforms coordinates in {@link MovementCommand}s by adding offsets in a given
* {@link CoordinateSystemTransformation}.
*/
public class CoordinateSystemMovementCommandTransformer
implements
MovementCommandTransformer {
private final CoordinateSystemTransformation transformation;
public CoordinateSystemMovementCommandTransformer(
@Nonnull
CoordinateSystemTransformation transformation
) {
this.transformation = requireNonNull(transformation, "transformation");
}
@Override
public MovementCommand apply(MovementCommand command) {
return command
.withTransportOrder(transformTransportOrder(command.getTransportOrder()))
.withDriveOrder(transformDriveOrder(command.getDriveOrder()))
.withFinalDestination(transformPoint(command.getFinalDestination()))
.withFinalDestinationLocation(transformLocation(command.getFinalDestinationLocation()))
.withOpLocation(transformLocation(command.getOpLocation()))
.withStep(transformStep(command.getStep()));
}
private TransportOrder transformTransportOrder(TransportOrder oldTransportOrder) {
return oldTransportOrder.withDriveOrders(
transformDriveOrders(oldTransportOrder.getAllDriveOrders())
);
}
private List<DriveOrder> transformDriveOrders(List<DriveOrder> oldDriveOrders) {
return oldDriveOrders.stream()
.map(this::transformDriveOrder)
.toList();
}
private DriveOrder transformDriveOrder(DriveOrder oldOrder) {
return oldOrder.withRoute(transformRoute(oldOrder.getRoute()));
}
private Route transformRoute(Route route) {
return Optional.ofNullable(route)
.map(
originalRoute -> new Route(
originalRoute.getSteps().stream()
.map(step -> transformStep(step))
.collect(Collectors.toList()),
originalRoute.getCosts()
)
)
.orElse(null);
}
private Step transformStep(Step oldStep) {
return new Step(
oldStep.getPath(),
transformPoint(oldStep.getSourcePoint()),
transformPoint(oldStep.getDestinationPoint()),
oldStep.getVehicleOrientation(),
oldStep.getRouteIndex(),
oldStep.isExecutionAllowed(),
oldStep.getReroutingType()
);
}
private Point transformPoint(Point point) {
return Optional.ofNullable(point)
.map(
originalPoint -> originalPoint.withPose(
new Pose(
transformTriple(originalPoint.getPose().getPosition()),
(originalPoint.getPose().getOrientationAngle() + transformation
.getOffsetOrientation()) % 360.0
)
)
)
.orElse(null);
}
private Location transformLocation(Location location) {
return Optional.ofNullable(location)
.map(
originalLocation -> originalLocation.withPosition(
transformTriple(originalLocation.getPosition())
)
)
.orElse(null);
}
private Triple transformTriple(Triple triple) {
return Optional.ofNullable(triple)
.map(
originalTriple -> new Triple(
originalTriple.getX() + transformation.getOffsetX(),
originalTriple.getY() + transformation.getOffsetY(),
originalTriple.getZ() + transformation.getOffsetZ()
)
)
.orElse(null);
}
}

View File

@@ -0,0 +1,177 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles.transformers;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import java.util.Optional;
import org.opentcs.data.model.Vehicle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The data used by coordinate system transformer classes.
* <p>
* The data consists of offset values that are ...
* </p>
* <ul>
* <li>added to coordinate and orientation angle values sent to the vehicle driver.</li>
* <li>subtracted from coordinate and orientation angle values reported by the vehicle driver.</li>
* </ul>
*/
public class CoordinateSystemTransformation {
/**
* The key of the property for the x offset.
*/
public static final String PROPKEY_OFFSET_X = "tcs:offsetTransformer.x";
/**
* The key of the property for the y offset.
*/
public static final String PROPKEY_OFFSET_Y = "tcs:offsetTransformer.y";
/**
* The key of the property for the z offset.
*/
public static final String PROPKEY_OFFSET_Z = "tcs:offsetTransformer.z";
/**
* The key of the property for the orientation offset.
*/
public static final String PROPKEY_OFFSET_ORIENTATION = "tcs:offsetTransformer.orientation";
private static final Logger LOG = LoggerFactory.getLogger(CoordinateSystemTransformation.class);
private final int offsetX;
private final int offsetY;
private final int offsetZ;
private final double offsetOrientation;
/**
* Creates a new instance.
*
* @param offsetX The x offset.
* @param offsetY The y offset.
* @param offsetZ The z offset.
* @param offsetOrientation The orientation offset.
*/
public CoordinateSystemTransformation(
int offsetX, int offsetY, int offsetZ, double offsetOrientation
) {
this.offsetX = offsetX;
this.offsetY = offsetY;
this.offsetZ = offsetZ;
this.offsetOrientation = offsetOrientation;
}
/**
* Returns the x offset.
*
* @return The x offset.
*/
public int getOffsetX() {
return offsetX;
}
/**
* Returns the y offset.
*
* @return The y offset.
*/
public int getOffsetY() {
return offsetY;
}
/**
* Returns the z offset.
*
* @return The z offset.
*/
public int getOffsetZ() {
return offsetZ;
}
/**
* Returns the orientation offset.
*
* @return The orientation offset.
*/
public double getOffsetOrientation() {
return offsetOrientation;
}
/**
* Creates a {@link CoordinateSystemTransformation} based on offsets given via vehicle properties.
* <p>
* The property keys used are the values of the following constants:
* </p>
* <ul>
* <li>{@link #PROPKEY_OFFSET_X}</li>
* <li>{@link #PROPKEY_OFFSET_Y}</li>
* <li>{@link #PROPKEY_OFFSET_Z}</li>
* <li>{@link #PROPKEY_OFFSET_ORIENTATION}</li>
* </ul>
*
* @param vehicle The vehicle.
* @return An {@link Optional} containing the {@link CoordinateSystemTransformation} or
* {@link Optional#empty()}, if one of the corresponding property values could not be parsed.
*/
public static Optional<CoordinateSystemTransformation> fromVehicle(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle, "vehicle");
try {
return Optional.of(
new CoordinateSystemTransformation(
getPropertyInteger(vehicle, PROPKEY_OFFSET_X, 0),
getPropertyInteger(vehicle, PROPKEY_OFFSET_Y, 0),
getPropertyInteger(vehicle, PROPKEY_OFFSET_Z, 0),
getPropertyDouble(vehicle, PROPKEY_OFFSET_ORIENTATION, 0.0)
)
);
}
catch (NumberFormatException e) {
LOG.warn(
"Could not create coordinate system transformation for vehicle '{}'.",
vehicle.getName(),
e
);
return Optional.empty();
}
}
private static int getPropertyInteger(Vehicle vehicle, String propertyKey, int defaultValue)
throws NumberFormatException {
String property = vehicle.getProperty(propertyKey);
if (property != null) {
return Integer.parseInt(property);
}
else {
LOG.debug(
"{}: Property '{}' is not set. Using default value {}.",
vehicle.getName(),
propertyKey,
defaultValue
);
return defaultValue;
}
}
private static double getPropertyDouble(Vehicle vehicle, String propertyKey, double defaultValue)
throws NumberFormatException {
String property = vehicle.getProperty(propertyKey);
if (property != null) {
return Double.parseDouble(property);
}
else {
LOG.debug(
"{}: Property '{}' is not set. Using default value {}.",
vehicle.getName(),
propertyKey,
defaultValue
);
return defaultValue;
}
}
}

View File

@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles.transformers;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.IncomingPoseTransformer;
import org.opentcs.drivers.vehicle.MovementCommandTransformer;
import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory;
/**
* Provides instances of {@link CoordinateSystemMovementCommandTransformer} and
* {@link CoordinateSystemIncomingPoseTransformer}.
*/
public class CoordinateSystemTransformerFactory
implements
VehicleDataTransformerFactory {
public CoordinateSystemTransformerFactory() {
}
@Override
@Nonnull
public String getName() {
return "OFFSET_TRANSFORMER";
}
@Override
@Nonnull
public MovementCommandTransformer createMovementCommandTransformer(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle);
return new CoordinateSystemMovementCommandTransformer(
CoordinateSystemTransformation.fromVehicle(vehicle)
.orElseThrow(
() -> new IllegalArgumentException(
"Cannot create transformer without transformation data."
)
)
);
}
@Override
@Nonnull
public IncomingPoseTransformer createIncomingPoseTransformer(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle);
return new CoordinateSystemIncomingPoseTransformer(
CoordinateSystemTransformation.fromVehicle(vehicle)
.orElseThrow(
() -> new IllegalArgumentException(
"Cannot create transformer without transformation data."
)
)
);
}
@Override
public boolean providesTransformersFor(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle, "vehicle");
return CoordinateSystemTransformation.fromVehicle(vehicle).isPresent();
}
}

View File

@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles.transformers;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.IncomingPoseTransformer;
import org.opentcs.drivers.vehicle.MovementCommandTransformer;
import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory;
/**
* Provides transformers that do not modify their inputs in any way.
*/
public class DefaultVehicleDataTransformerFactory
implements
VehicleDataTransformerFactory {
public DefaultVehicleDataTransformerFactory() {
}
@Override
@Nonnull
public String getName() {
return "DEFAULT_TRANSFORMER";
}
@Override
@Nonnull
public MovementCommandTransformer createMovementCommandTransformer(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle, "vehicle");
return command -> command;
}
@Override
@Nonnull
public IncomingPoseTransformer createIncomingPoseTransformer(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle, "vehicle");
return pose -> pose;
}
@Override
public boolean providesTransformersFor(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle, "vehicle");
return true;
}
}

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.vehicles.transformers;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkState;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import java.util.Optional;
import java.util.Set;
import org.opentcs.components.Lifecycle;
import org.opentcs.data.ObjectPropConstants;
import org.opentcs.data.model.Vehicle;
import org.opentcs.drivers.vehicle.VehicleDataTransformerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A registry for all vehicle data transformers in the system.
*/
public class VehicleDataTransformerRegistry
implements
Lifecycle {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(VehicleDataTransformerRegistry.class);
/**
* The registered factories.
*/
private final Set<VehicleDataTransformerFactory> factories;
/**
* Indicates whether this component is initialized or not.
*/
private boolean initialized;
/**
* Creates a new registry.
*
* @param factories The data transformer factories.
*/
@Inject
public VehicleDataTransformerRegistry(
@Nonnull
Set<VehicleDataTransformerFactory> factories
) {
this.factories = requireNonNull(factories, "factories");
checkState(!factories.isEmpty(), "No adapter factories found.");
}
@Override
public void initialize() {
if (isInitialized()) {
LOG.debug("Already initialized.");
return;
}
initialized = true;
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public void terminate() {
if (!isInitialized()) {
LOG.debug("Not initialized.");
return;
}
initialized = false;
}
/**
* Returns a factory for data transformers for the given vehicle.
*
* @param vehicle The vehicle to find a data transformer factory for.
* @return A factory for data transformers for the given vehicle.
*/
public VehicleDataTransformerFactory findFactoryFor(
@Nonnull
Vehicle vehicle
) {
requireNonNull(vehicle, "vehicle");
return Optional.ofNullable(vehicle.getProperty(ObjectPropConstants.VEHICLE_DATA_TRANSFORMER))
.flatMap(
name -> factories.stream()
.filter(factory -> name.equals(factory.getName()))
.filter(factory -> factory.providesTransformersFor(vehicle))
.findAny()
)
.orElseGet(() -> {
LOG.debug("Falling back to default transformer for vehicle '{}'", vehicle.getName());
return new DefaultVehicleDataTransformerFactory();
});
}
}

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.Set;
import org.opentcs.components.kernel.OrderSequenceCleanupApproval;
import org.opentcs.data.order.OrderSequence;
/**
* A collection of {@link OrderSequenceCleanupApproval}s.
*/
public class CompositeOrderSequenceCleanupApproval
implements
OrderSequenceCleanupApproval {
private final Set<OrderSequenceCleanupApproval> sequenceCleanupApprovals;
private final DefaultOrderSequenceCleanupApproval defaultOrderSequenceCleanupApproval;
/**
* Creates a new instance.
*
* @param sequenceCleanupApprovals The {@link OrderSequenceCleanupApproval}s.
* @param defaultOrderSequenceCleanupApproval The {@link OrderSequenceCleanupApproval}, which
* should always be applied by default.
*/
@Inject
public CompositeOrderSequenceCleanupApproval(
Set<OrderSequenceCleanupApproval> sequenceCleanupApprovals,
DefaultOrderSequenceCleanupApproval defaultOrderSequenceCleanupApproval
) {
this.sequenceCleanupApprovals = requireNonNull(
sequenceCleanupApprovals,
"sequenceCleanupApprovals"
);
this.defaultOrderSequenceCleanupApproval
= requireNonNull(
defaultOrderSequenceCleanupApproval,
"defaultOrderSequenceCleanupApproval"
);
}
@Override
public boolean test(OrderSequence seq) {
if (!defaultOrderSequenceCleanupApproval.test(seq)) {
return false;
}
for (OrderSequenceCleanupApproval approval : sequenceCleanupApprovals) {
if (!approval.test(seq)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.Set;
import org.opentcs.components.kernel.PeripheralJobCleanupApproval;
import org.opentcs.data.peripherals.PeripheralJob;
/**
* A collection of {@link PeripheralJobCleanupApproval}s.
*/
public class CompositePeripheralJobCleanupApproval
implements
PeripheralJobCleanupApproval {
private final Set<PeripheralJobCleanupApproval> peripheralJobCleanupApprovals;
private final DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval;
/**
* Creates a new instance.
*
* @param peripheralJobCleanupApprovals The {@link PeripheralJobCleanupApproval}s.
* @param defaultPeripheralJobCleanupApproval The {@link PeripheralJobCleanupApproval}, which
* should always be applied by default.
*/
@Inject
public CompositePeripheralJobCleanupApproval(
Set<PeripheralJobCleanupApproval> peripheralJobCleanupApprovals,
DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval
) {
this.peripheralJobCleanupApprovals = requireNonNull(
peripheralJobCleanupApprovals,
"peripheralJobCleanupApprovals"
);
this.defaultPeripheralJobCleanupApproval
= requireNonNull(
defaultPeripheralJobCleanupApproval,
"defaultPeripheralJobCleanupApproval"
);
}
@Override
public boolean test(PeripheralJob job) {
if (!defaultPeripheralJobCleanupApproval.test(job)) {
return false;
}
for (PeripheralJobCleanupApproval approval : peripheralJobCleanupApprovals) {
if (!approval.test(job)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.Set;
import org.opentcs.components.kernel.TransportOrderCleanupApproval;
import org.opentcs.data.order.TransportOrder;
/**
* A collection of {@link TransportOrderCleanupApproval}s.
*/
public class CompositeTransportOrderCleanupApproval
implements
TransportOrderCleanupApproval {
private final Set<TransportOrderCleanupApproval> orderCleanupApprovals;
private final DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval;
/**
* Creates a new instance.
*
* @param orderCleanupApprovals The {@link TransportOrderCleanupApproval}s.
* @param defaultTransportOrderCleanupApproval The {@link TransportOrderCleanupApproval}, which
* should always be applied by default.
*/
@Inject
public CompositeTransportOrderCleanupApproval(
Set<TransportOrderCleanupApproval> orderCleanupApprovals,
DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval
) {
this.orderCleanupApprovals = requireNonNull(orderCleanupApprovals, "orderCleanupApprovals");
this.defaultTransportOrderCleanupApproval
= requireNonNull(
defaultTransportOrderCleanupApproval,
"defaultTransportOrderCleanupApproval"
);
}
@Override
public boolean test(TransportOrder order) {
if (!defaultTransportOrderCleanupApproval.test(order)) {
return false;
}
for (TransportOrderCleanupApproval approval : orderCleanupApprovals) {
if (!approval.test(order)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import jakarta.inject.Inject;
import java.time.Instant;
/**
* Keeps track of the time used to determine whether a working set item should be removed (according
* to its creation time).
*/
public class CreationTimeThreshold {
private Instant currentThreshold;
/**
* Creates a new instance.
*/
@Inject
public CreationTimeThreshold() {
}
/**
* Updates the current threshold by subtracting the given amount of milliseconds from the current
* time.
*
* @param millis The amount of milliseconds to subtract from the current time.
*/
public void updateCurrentThreshold(long millis) {
currentThreshold = Instant.now().minusMillis(millis);
}
/**
* Returns the current threshold.
* <p>
* Working set items that are created before this point of time should be removed.
*
* @return The current threshold.
*/
public Instant getCurrentThreshold() {
return currentThreshold;
}
}

View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import org.opentcs.components.kernel.OrderSequenceCleanupApproval;
import org.opentcs.data.order.OrderSequence;
import org.opentcs.data.order.TransportOrder;
/**
* Checks whether an order sequence may be removed.
*/
public class DefaultOrderSequenceCleanupApproval
implements
OrderSequenceCleanupApproval {
private final TransportOrderPoolManager orderPoolManager;
private final DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval;
/**
* Creates a new instance.
*
* @param orderPoolManager The order pool manager to be used.
* @param defaultTransportOrderCleanupApproval Checks whether a transport order may be removed.
*/
@Inject
public DefaultOrderSequenceCleanupApproval(
TransportOrderPoolManager orderPoolManager,
DefaultTransportOrderCleanupApproval defaultTransportOrderCleanupApproval
) {
this.orderPoolManager = requireNonNull(orderPoolManager, "orderPoolManager");
this.defaultTransportOrderCleanupApproval
= requireNonNull(
defaultTransportOrderCleanupApproval,
"defaultTransportOrderCleanupApproval"
);
}
@Override
public boolean test(OrderSequence seq) {
if (!seq.isFinished()) {
return false;
}
if (hasUnapprovedOrder(seq)) {
return false;
}
return true;
}
private boolean hasUnapprovedOrder(OrderSequence seq) {
return !(seq.getOrders()
.stream()
.map(
reference -> orderPoolManager.getObjectRepo()
.getObject(TransportOrder.class, reference)
)
.allMatch(defaultTransportOrderCleanupApproval));
}
}

View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import org.opentcs.components.kernel.PeripheralJobCleanupApproval;
import org.opentcs.data.peripherals.PeripheralJob;
/**
* Checks whether a peripheral job may be removed.
*/
public class DefaultPeripheralJobCleanupApproval
implements
PeripheralJobCleanupApproval {
private final CreationTimeThreshold creationTimeThreshold;
/**
* Creates a new instance.
*
* @param creationTimeThreshold Keeps track of the time used to determine whether a peripheral
* job should be removed (according to its creation time).
*/
@Inject
public DefaultPeripheralJobCleanupApproval(CreationTimeThreshold creationTimeThreshold) {
this.creationTimeThreshold = requireNonNull(creationTimeThreshold, "creationTimeThreshold");
}
@Override
public boolean test(PeripheralJob job) {
if (!job.getState().isFinalState()) {
return false;
}
if (job.getCreationTime().isAfter(creationTimeThreshold.getCurrentThreshold())) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import jakarta.inject.Inject;
import java.util.Objects;
import org.opentcs.components.kernel.TransportOrderCleanupApproval;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.data.peripherals.PeripheralJob;
/**
* Checks whether a transport order may be removed.
*/
public class DefaultTransportOrderCleanupApproval
implements
TransportOrderCleanupApproval {
private final PeripheralJobPoolManager peripheralJobPoolManager;
private final DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval;
private final CreationTimeThreshold creationTimeThreshold;
/**
* Creates a new instance.
*
* @param peripheralJobPoolManager The peripheral job pool manager to be used.
* @param defaultPeripheralJobCleanupApproval Checks whether a peripheral job may be removed.
* @param creationTimeThreshold Keeps track of the time used to determine whether a transport
* order should be removed (according to its creation time).
*/
@Inject
public DefaultTransportOrderCleanupApproval(
PeripheralJobPoolManager peripheralJobPoolManager,
DefaultPeripheralJobCleanupApproval defaultPeripheralJobCleanupApproval,
CreationTimeThreshold creationTimeThreshold
) {
this.peripheralJobPoolManager = requireNonNull(
peripheralJobPoolManager,
"peripheralJobPoolManager"
);
this.defaultPeripheralJobCleanupApproval
= requireNonNull(
defaultPeripheralJobCleanupApproval,
"defaultPeripheralJobCleanupApproval"
);
this.creationTimeThreshold = requireNonNull(creationTimeThreshold, "creationTimeThreshold");
}
@Override
public boolean test(TransportOrder order) {
if (!order.getState().isFinalState()) {
return false;
}
if (isRelatedToJobWithNonFinalState(order)) {
return false;
}
if (isRelatedToUnapprovedJob(order)) {
return false;
}
if (order.getCreationTime().isAfter(creationTimeThreshold.getCurrentThreshold())) {
return false;
}
return true;
}
private boolean isRelatedToJobWithNonFinalState(TransportOrder order) {
return peripheralJobPoolManager.getObjectRepo()
.getObjects(
PeripheralJob.class,
job -> Objects.equals(job.getRelatedTransportOrder(), order.getReference())
)
.stream()
.filter(job -> !job.getState().isFinalState())
.findAny()
.isPresent();
}
private boolean isRelatedToUnapprovedJob(TransportOrder order) {
return !(peripheralJobPoolManager.getObjectRepo().getObjects(
PeripheralJob.class,
job -> Objects.equals(job.getRelatedTransportOrder(), order.getReference())
)
.stream()
.allMatch(defaultPeripheralJobCleanupApproval));
}
}

View File

@@ -0,0 +1,151 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkInRange;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.opentcs.access.NotificationPublicationEvent;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.notification.UserNotification;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A buffer which can store a (configurable) limited number of
* {@link UserNotification UserNotification} objects.
* <p>
* When a new message is added to the buffer and the number of messages in the buffer exceeds its
* <code>capacity</code>, messages are removed from the buffer until it contains not more than
* <code>capacity</code>.
* </p>
* <p>
* Note that no synchronization is done inside this class. Concurrent access of
* instances of this class must be synchronized externally.
* </p>
*/
public class NotificationBuffer {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(NotificationBuffer.class);
/**
* The actual messages.
*/
private final Queue<UserNotification> notifications = new ArrayDeque<>();
/**
* The maximum number of messages that should be kept in this buffer.
*/
private int capacity = 500;
/**
* A listener for events concerning the stored messages.
*/
private final EventHandler messageEventListener;
/**
* Creates a new instance that uses the given event listener.
*
* @param eventListener The event listener to be used.
*/
@Inject
public NotificationBuffer(
@ApplicationEventBus
EventHandler eventListener
) {
messageEventListener = requireNonNull(eventListener, "eventListener");
}
/**
* Returns this buffer's capacity.
*
* @return This buffer's capacity.
*/
public int getCapacity() {
return capacity;
}
/**
* Adjusts this buffer's <code>capacity</code>.
* If the new capacity is less than the current number of messages in this
* buffer, messages are removed until the number of messages equals the
* buffer's <code>capacity</code>.
*
* @param capacity The buffer's new capacity. Must be at least 1.
* @throws IllegalArgumentException If <code>newCapacity</code> is less than 1.
*/
public void setCapacity(int capacity) {
this.capacity = checkInRange(capacity, 1, Integer.MAX_VALUE, "capacity");
cutBackMessages();
}
/**
* Adds a notification to the buffer.
*
* @param notification The notification to be added.
*/
public void addNotification(UserNotification notification) {
requireNonNull(notification, "notification");
notifications.add(notification);
LOG.info("User notification added: {}", notification);
// Make sure we don't have too many messages now.
cutBackMessages();
// Emit an event for this message.
emitMessageEvent(notification);
}
/**
* Returns all notifications that are accepted by the given filter, or all notifications, if no
* filter is given.
*
* @param predicate The predicate used to filter. May be <code>null</code> to return all
* notifications.
* @return A list of notifications accepted by the given filter.
*/
public List<UserNotification> getNotifications(
@Nullable
Predicate<UserNotification> predicate
) {
Predicate<UserNotification> filterPredicate
= predicate == null
? (notification) -> true
: predicate;
return notifications.stream()
.filter(filterPredicate)
.collect(Collectors.toCollection(ArrayList::new));
}
/**
* Removes all messages from this buffer.
*/
public void clear() {
notifications.clear();
}
/**
* Emits an event for the given message.
*
* @param message The message to emit an event for.
*/
public void emitMessageEvent(UserNotification message) {
messageEventListener.onEvent(new NotificationPublicationEvent(message));
}
private void cutBackMessages() {
while (notifications.size() > capacity) {
notifications.remove();
}
}
}

View File

@@ -0,0 +1,239 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import org.opentcs.access.to.peripherals.PeripheralJobCreationTO;
import org.opentcs.access.to.peripherals.PeripheralOperationCreationTO;
import org.opentcs.components.kernel.ObjectNameProvider;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.ObjectExistsException;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObjectEvent;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.TCSResourceReference;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.data.peripherals.PeripheralJob;
import org.opentcs.data.peripherals.PeripheralOperation;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Keeps all {@code PeripheralJobs}s and provides methods to create and manipulate them.
* <p>
* Note that no synchronization is done inside this class. Concurrent access of instances of this
* class must be synchronized externally.
* </p>
*/
public class PeripheralJobPoolManager
extends
TCSObjectManager {
/**
* This class's logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(PeripheralJobPoolManager.class);
/**
* Provides names for peripheral jobs.
*/
private final ObjectNameProvider objectNameProvider;
/**
* Creates a new instance.
*
* @param objectRepo The object repo.
* @param eventHandler The event handler to publish events to.
* @param orderNameProvider Provides names for peripheral jobs.
*/
@Inject
public PeripheralJobPoolManager(
@Nonnull
TCSObjectRepository objectRepo,
@Nonnull
@ApplicationEventBus
EventHandler eventHandler,
@Nonnull
ObjectNameProvider orderNameProvider
) {
super(objectRepo, eventHandler);
this.objectNameProvider = requireNonNull(orderNameProvider, "orderNameProvider");
}
/**
* Removes all peripheral jobs from this pool.
*/
public void clear() {
for (PeripheralJob job : getObjectRepo().getObjects(PeripheralJob.class)) {
getObjectRepo().removeObject(job.getReference());
emitObjectEvent(
null,
job,
TCSObjectEvent.Type.OBJECT_REMOVED
);
}
}
/**
* Adds a new peripheral job to the pool.
*
* @param to The transfer object from which to create the new peripheral job.
* @return The newly created peripheral job.
* @throws ObjectExistsException If an object with the new object's name already exists.
* @throws ObjectUnknownException If any object referenced in the TO does not exist.
* @throws IllegalArgumentException If the transfer object's combination of parameters is invalid.
*/
public PeripheralJob createPeripheralJob(PeripheralJobCreationTO to)
throws ObjectUnknownException,
ObjectExistsException,
IllegalArgumentException {
checkArgument(
!hasCompletionRequiredAndExecutionTriggerImmediate(to),
"Peripheral job's operation has executionTrigger 'immediate' and completionRequired set."
);
PeripheralJob job = new PeripheralJob(
nameFor(to),
to.getReservationToken(),
toPeripheralOperation(to.getPeripheralOperation())
)
.withRelatedVehicle(toVehicleReference(to.getRelatedVehicleName()))
.withRelatedTransportOrder(toTransportOrderReference(to.getRelatedTransportOrderName()))
.withProperties(to.getProperties());
LOG.info(
"Peripheral job is being created: {} -- {}",
job.getName(),
job.getPeripheralOperation()
);
getObjectRepo().addObject(job);
emitObjectEvent(job, null, TCSObjectEvent.Type.OBJECT_CREATED);
return job;
}
/**
* Sets a peripheral jobs's state.
*
* @param ref A reference to the peripheral job to be modified.
* @param newState The peripheral job's new state.
* @return The modified peripheral job.
* @throws ObjectUnknownException If the referenced peripheral job is not in this pool.
*/
public PeripheralJob setPeripheralJobState(
TCSObjectReference<PeripheralJob> ref,
PeripheralJob.State newState
)
throws ObjectUnknownException {
PeripheralJob previousState = getObjectRepo().getObject(PeripheralJob.class, ref);
checkArgument(
!previousState.getState().isFinalState(),
"Peripheral job %s already in a final state, not changing %s -> %s.",
ref.getName(),
previousState.getState(),
newState
);
LOG.info(
"Peripheral job's state changes: {} -- {} -> {}",
previousState.getName(),
previousState.getState(),
newState
);
PeripheralJob job = previousState.withState(newState);
getObjectRepo().replaceObject(job);
emitObjectEvent(
job,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return job;
}
/**
* Removes the referenced peripheral job from the pool.
*
* @param ref A reference to the peripheral job to be removed.
* @return The removed peripheral job.
* @throws ObjectUnknownException If the referenced peripheral job is not in the pool.
*/
public PeripheralJob removePeripheralJob(TCSObjectReference<PeripheralJob> ref)
throws ObjectUnknownException {
PeripheralJob job = getObjectRepo().getObject(PeripheralJob.class, ref);
// Make sure only jobs in a final state are removed.
checkArgument(
job.getState().isFinalState(),
"Peripheral job %s is not in a final state.",
job.getName()
);
getObjectRepo().removeObject(ref);
emitObjectEvent(
null,
job,
TCSObjectEvent.Type.OBJECT_REMOVED
);
return job;
}
private PeripheralOperation toPeripheralOperation(PeripheralOperationCreationTO to)
throws ObjectUnknownException {
return new PeripheralOperation(
toLocationReference(to.getLocationName()),
to.getOperation(),
to.getExecutionTrigger(),
to.isCompletionRequired()
);
}
private TCSResourceReference<Location> toLocationReference(String locationName)
throws ObjectUnknownException {
Location location = getObjectRepo().getObject(Location.class, locationName);
return location.getReference();
}
private TCSObjectReference<Vehicle> toVehicleReference(String vehicleName)
throws ObjectUnknownException {
if (vehicleName == null) {
return null;
}
Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleName);
return vehicle.getReference();
}
private TCSObjectReference<TransportOrder> toTransportOrderReference(String transportOrderName)
throws ObjectUnknownException {
if (transportOrderName == null) {
return null;
}
TransportOrder order = getObjectRepo().getObject(TransportOrder.class, transportOrderName);
return order.getReference();
}
@Nonnull
private String nameFor(
@Nonnull
PeripheralJobCreationTO to
) {
if (to.hasIncompleteName()) {
return objectNameProvider.apply(to);
}
else {
return to.getName();
}
}
private boolean hasCompletionRequiredAndExecutionTriggerImmediate(PeripheralJobCreationTO to) {
PeripheralOperationCreationTO opTo = to.getPeripheralOperation();
return opTo.isCompletionRequired()
&& opTo.getExecutionTrigger() == PeripheralOperation.ExecutionTrigger.IMMEDIATE;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import de.huxhorn.sulky.ulid.ULID;
import java.util.Optional;
import org.opentcs.access.to.CreationTO;
import org.opentcs.components.kernel.ObjectNameProvider;
/**
* Provides names for objects based on ULIDs, prefixed with the name taken from a given
* {@link CreationTO}.
*/
public class PrefixedUlidObjectNameProvider
implements
ObjectNameProvider {
/**
* Generates ULIDs for us.
*/
private final ULID ulid = new ULID();
/**
* The previously generated ULID value.
*/
private ULID.Value previousUlid = ulid.nextValue();
/**
* Creates a new instance.
*/
public PrefixedUlidObjectNameProvider() {
}
@Override
public String apply(CreationTO to) {
requireNonNull(to, "to");
Optional<ULID.Value> newValue = ulid.nextStrictlyMonotonicValue(previousUlid);
while (newValue.isEmpty()) {
newValue = ulid.nextStrictlyMonotonicValue(previousUlid);
}
previousUlid = newValue.get();
return to.getName() + newValue.get().toString();
}
}

View File

@@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.ObjectHistory;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObject;
import org.opentcs.data.TCSObjectEvent;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles generic modifications of objects contained in a {@link TCSObjectRepository}.
* <p>
* Note that no synchronization is done inside this class. Concurrent access of instances of this
* class must be synchronized externally.
* </p>
*/
public class TCSObjectManager {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(TCSObjectManager.class);
/**
* The object repo.
*/
private final TCSObjectRepository objectRepo;
/**
* A handler we should emit object events to.
*/
private final EventHandler eventHandler;
/**
* Creates a new instance.
*
* @param objectRepo The object repo.
* @param eventHandler The event handler to publish events to.
*/
@Inject
public TCSObjectManager(
@Nonnull
TCSObjectRepository objectRepo,
@Nonnull
@ApplicationEventBus
EventHandler eventHandler
) {
this.objectRepo = requireNonNull(objectRepo, "objectRepo");
this.eventHandler = requireNonNull(eventHandler, "eventHandler");
}
/**
* Returns the underlying object repo.
*
* @return The underlying object repo.
*/
@Nonnull
public TCSObjectRepository getObjectRepo() {
return objectRepo;
}
/**
* Sets a property for the referenced object.
*
* @param ref A reference to the object to be modified.
* @param key The property's key/name.
* @param value The property's value. If <code>null</code>, removes the
* property from the object.
* @throws ObjectUnknownException If the referenced object does not exist.
*/
public void setObjectProperty(
@Nonnull
TCSObjectReference<?> ref,
@Nonnull
String key,
@Nullable
String value
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(key, "key");
TCSObject<?> object = objectRepo.getObject(ref);
TCSObject<?> previousState = object;
LOG.debug(
"Setting property on object named '{}': key='{}', value='{}'",
ref.getName(),
key,
value
);
object = object.withProperty(key, value);
objectRepo.replaceObject(object);
emitObjectEvent(object, previousState, TCSObjectEvent.Type.OBJECT_MODIFIED);
}
/**
* Appends a history entry to the referenced object.
*
* @param ref A reference to the object to be modified.
* @param entry The history entry to be appended.
* @throws ObjectUnknownException If the referenced object does not exist.
*/
public void appendObjectHistoryEntry(
@Nonnull
TCSObjectReference<?> ref,
@Nonnull
ObjectHistory.Entry entry
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
requireNonNull(entry, "entry");
TCSObject<?> object = objectRepo.getObject(ref);
TCSObject<?> previousState = object;
LOG.debug("Appending history entry to object named '{}': {}", ref.getName(), entry);
object = object.withHistoryEntry(entry);
objectRepo.replaceObject(object);
emitObjectEvent(object, previousState, TCSObjectEvent.Type.OBJECT_MODIFIED);
}
/**
* Emits an event for the given object with the given type.
*
* @param currentObjectState The current state of the object to emit an event
* for.
* @param previousObjectState The previous state of the object to emit an
* event for.
* @param evtType The type of event to emit.
*/
public void emitObjectEvent(
TCSObject<?> currentObjectState,
TCSObject<?> previousObjectState,
TCSObjectEvent.Type evtType
) {
eventHandler.onEvent(new TCSObjectEvent(currentObjectState, previousObjectState, evtType));
}
}

View File

@@ -0,0 +1,350 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.opentcs.data.ObjectExistsException;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObject;
import org.opentcs.data.TCSObjectReference;
/**
* A container for <code>TCSObject</code>s belonging together.
* <p>
* Provides access to a set of data objects and ensures they have unique names.
* </p>
*/
public class TCSObjectRepository {
/**
* The objects contained in this pool, mapped by their names, grouped by their classes.
*/
private final Map<Class<?>, Map<String, TCSObject<?>>> objects = new HashMap<>();
/**
* Creates a new instance.
*/
public TCSObjectRepository() {
}
/**
* Adds a new object to the pool.
*
* @param newObject The object to be added to the pool.
* @throws ObjectExistsException If an object with the same ID or the same
* name as the new one already exists in this pool.
*/
public void addObject(
@Nonnull
TCSObject<?> newObject
)
throws ObjectExistsException {
requireNonNull(newObject, "newObject");
if (containsName(newObject.getName())) {
throw new ObjectExistsException("Object name already exists: " + newObject.getName());
}
Map<String, TCSObject<?>> objectsByName = objects.get(newObject.getClass());
if (objectsByName == null) {
objectsByName = new HashMap<>();
objects.put(newObject.getClass(), objectsByName);
}
objectsByName.put(newObject.getName(), newObject);
}
/**
* Uses the given object to replace an object in the pool with same name.
*
* @param object The replacing object.
* @throws IllegalArgumentException If an object with the same name as the given object does not
* exist in this repository, yet, or if an object with the same name does exist but is an instance
* of a different class.
*/
@Nonnull
public void replaceObject(
@Nonnull
TCSObject<?> object
)
throws IllegalArgumentException {
requireNonNull(object, "object");
TCSObject<?> oldObject = getObjectOrNull(object.getName());
checkArgument(
oldObject != null,
"Object named '%s' does not exist",
object.getName()
);
checkArgument(
object.getClass() == oldObject.getClass(),
"Object named '%s' not an instance of the same class: '%s' != '%s'",
object.getName(),
object.getClass().getName(),
oldObject.getClass().getName()
);
objects.get(object.getClass()).put(object.getName(), object);
}
/**
* Returns an object from the pool.
*
* @param ref A reference to the object to return.
* @return The referenced object, or <code>null</code>, if no such object exists in this pool.
*/
@Nullable
public TCSObject<?> getObjectOrNull(
@Nonnull
TCSObjectReference<?> ref
) {
requireNonNull(ref);
return objects.getOrDefault(ref.getReferentClass(), Map.of()).get(ref.getName());
}
/**
* Returns an object from the pool.
*
* @param ref A reference to the object to return.
* @return The referenced object.
* @throws ObjectUnknownException If the referenced object does not exist.
*/
@Nonnull
public TCSObject<?> getObject(
@Nonnull
TCSObjectReference<?> ref
)
throws ObjectUnknownException {
TCSObject<?> result = getObjectOrNull(ref);
if (result == null) {
throw new ObjectUnknownException(ref);
}
return result;
}
/**
* Returns an object from the pool.
*
* @param <T> The object's type.
* @param clazz The class of the object to be returned.
* @param ref A reference to the object to be returned.
* @return The referenced object, or <code>null</code>, if no such object
* exists in this pool or if an object exists but is not an instance of the
* given class.
*/
@Nullable
public <T extends TCSObject<T>> T getObjectOrNull(
@Nonnull
Class<T> clazz,
@Nonnull
TCSObjectReference<T> ref
) {
requireNonNull(clazz, "clazz");
requireNonNull(ref, "ref");
TCSObject<?> result = objects.getOrDefault(clazz, Map.of()).get(ref.getName());
if (clazz.isInstance(result)) {
return clazz.cast(result);
}
else {
return null;
}
}
/**
* Returns an object from the pool.
*
* @param <T> The object's type.
* @param clazz The class of the object to be returned.
* @param ref A reference to the object to be returned.
* @return The referenced object.
* @throws ObjectUnknownException If the referenced object does not exist, or if an object exists
* but is not an instance of the given class.
*/
@Nonnull
public <T extends TCSObject<T>> T getObject(
@Nonnull
Class<T> clazz,
@Nonnull
TCSObjectReference<T> ref
)
throws ObjectUnknownException {
T result = getObjectOrNull(clazz, ref);
if (result == null) {
throw new ObjectUnknownException(ref);
}
return result;
}
/**
* Returns an object from the pool.
*
* @param name The name of the object to return.
* @return The object with the given name, or <code>null</code>, if no such
* object exists in this pool.
*/
@Nullable
public TCSObject<?> getObjectOrNull(
@Nonnull
String name
) {
requireNonNull(name, "name");
return objects.values().stream()
.map(objectsByName -> objectsByName.get(name))
.filter(object -> object != null)
.findAny()
.orElse(null);
}
/**
* Returns an object from the pool.
*
* @param name The name of the object to return.
* @return The object with the given name.
* @throws ObjectUnknownException If the referenced object does not exist.
*/
@Nonnull
public TCSObject<?> getObject(
@Nonnull
String name
)
throws ObjectUnknownException {
TCSObject<?> result = getObjectOrNull(name);
if (result == null) {
throw new ObjectUnknownException(name);
}
return result;
}
/**
* Returns an object from the pool.
*
* @param <T> The object's type.
* @param clazz The class of the object to be returned.
* @param name The name of the object to be returned.
* @return The named object, or <code>null</code>, if no such object
* exists in this pool or if an object exists but is not an instance of the
* given class.
*/
@Nullable
public <T extends TCSObject<T>> T getObjectOrNull(
@Nonnull
Class<T> clazz,
@Nonnull
String name
) {
requireNonNull(clazz, "clazz");
requireNonNull(name, "name");
TCSObject<?> result = objects.getOrDefault(clazz, Map.of()).get(name);
if (clazz.isInstance(result)) {
return clazz.cast(result);
}
else {
return null;
}
}
/**
* Returns an object from the pool.
*
* @param <T> The object's type.
* @param clazz The class of the object to be returned.
* @param name The name of the object to be returned.
* @return The named object.
* @throws ObjectUnknownException If no object with the given name exists in this pool or if an
* object exists but is not an instance of the given class.
*/
@Nonnull
public <T extends TCSObject<T>> T getObject(
@Nonnull
Class<T> clazz,
@Nonnull
String name
)
throws ObjectUnknownException {
T result = getObjectOrNull(clazz, name);
if (result == null) {
throw new ObjectUnknownException(name);
}
return result;
}
/**
* Returns a set of objects belonging to the given class.
*
* @param <T> The objects' type.
* @param clazz The class of the objects to be returned.
* @return A set of objects belonging to the given class.
*/
@Nonnull
public <T extends TCSObject<T>> Set<T> getObjects(
@Nonnull
Class<T> clazz
) {
return objects.getOrDefault(clazz, Map.of()).values().stream()
.map(object -> clazz.cast(object))
.collect(Collectors.toSet());
}
/**
* Returns a set of objects of the given class for which the given predicate is true.
*
* @param <T> The objects' type.
* @param clazz The class of the objects to be returned.
* @param predicate The predicate that must be true for returned objects.
* @return A set of objects of the given class for which the given predicate is true. If no such
* objects exist, the returned set is empty.
*/
@Nonnull
public <T extends TCSObject<T>> Set<T> getObjects(
@Nonnull
Class<T> clazz,
@Nonnull
Predicate<? super T> predicate
) {
requireNonNull(clazz, "clazz");
requireNonNull(predicate, "predicate");
return objects.getOrDefault(clazz, Map.of()).values().stream()
.map(object -> clazz.cast(object))
.filter(predicate)
.collect(Collectors.toSet());
}
/**
* Removes a referenced object from this pool.
*
* @param ref A reference to the object to be removed.
* @return The object that was removed from the pool.
* @throws ObjectUnknownException If the referenced object does not exist.
*/
@Nonnull
public TCSObject<?> removeObject(
@Nonnull
TCSObjectReference<?> ref
)
throws ObjectUnknownException {
requireNonNull(ref, "ref");
Map<String, TCSObject<?>> map = objects.get(ref.getReferentClass());
TCSObject<?> obj = (map == null) ? null : map.remove(ref.getName());
if (obj == null) {
throw new ObjectUnknownException(ref);
}
return obj;
}
private boolean containsName(String name) {
return objects.values().stream().anyMatch(objectsByName -> objectsByName.containsKey(name));
}
}

View File

@@ -0,0 +1,817 @@
// SPDX-FileCopyrightText: The openTCS Authors
// SPDX-License-Identifier: MIT
package org.opentcs.kernel.workingset;
import static java.util.Objects.requireNonNull;
import static org.opentcs.util.Assertions.checkArgument;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.opentcs.access.to.order.DestinationCreationTO;
import org.opentcs.access.to.order.OrderSequenceCreationTO;
import org.opentcs.access.to.order.TransportOrderCreationTO;
import org.opentcs.components.kernel.ObjectNameProvider;
import org.opentcs.customizations.ApplicationEventBus;
import org.opentcs.data.ObjectExistsException;
import org.opentcs.data.ObjectUnknownException;
import org.opentcs.data.TCSObject;
import org.opentcs.data.TCSObjectEvent;
import org.opentcs.data.TCSObjectReference;
import org.opentcs.data.model.Location;
import org.opentcs.data.model.Location.Link;
import org.opentcs.data.model.LocationType;
import org.opentcs.data.model.Point;
import org.opentcs.data.model.Vehicle;
import org.opentcs.data.order.DriveOrder;
import org.opentcs.data.order.DriveOrder.Destination;
import org.opentcs.data.order.OrderSequence;
import org.opentcs.data.order.TransportOrder;
import org.opentcs.util.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Keeps all {@code TransportOrder}s and provides methods to create and manipulate them.
* <p>
* Note that no synchronization is done inside this class. Concurrent access of instances of this
* class must be synchronized externally.
* </p>
*/
public class TransportOrderPoolManager
extends
TCSObjectManager {
/**
* This class's Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(TransportOrderPoolManager.class);
/**
* Provides names for transport orders and order sequences.
*/
private final ObjectNameProvider objectNameProvider;
/**
* Creates a new instance.
*
* @param objectRepo The object repo.
* @param eventHandler The event handler to publish events to.
* @param orderNameProvider Provides names for transport orders.
*/
@Inject
public TransportOrderPoolManager(
@Nonnull
TCSObjectRepository objectRepo,
@Nonnull
@ApplicationEventBus
EventHandler eventHandler,
@Nonnull
ObjectNameProvider orderNameProvider
) {
super(objectRepo, eventHandler);
this.objectNameProvider = requireNonNull(orderNameProvider, "orderNameProvider");
}
/**
* Removes all transport orders from this pool.
*/
public void clear() {
List<TCSObject<?>> objects = new ArrayList<>();
objects.addAll(getObjectRepo().getObjects(OrderSequence.class));
objects.addAll(getObjectRepo().getObjects(TransportOrder.class));
for (TCSObject<?> curObject : objects) {
getObjectRepo().removeObject(curObject.getReference());
emitObjectEvent(
null,
curObject,
TCSObjectEvent.Type.OBJECT_REMOVED
);
}
}
/**
* Adds a new transport order to the pool.
* This method implicitly adds the transport order to its wrapping sequence, if any.
*
* @param to The transfer object from which to create the new transport order.
* @return The newly created transport order.
* @throws ObjectExistsException If an object with the new object's name already exists.
* @throws ObjectUnknownException If any object referenced in the TO does not exist.
* @throws IllegalArgumentException One of:
* <ol>
* <li>The order is supposed to be part of an order sequence, but
* the sequence is already complete.</li>
* <li>The type of the transport order and the order sequence differ.</li>
* <li>The intended vehicle of the transport order and the order sequence differ.</li>
* <li>A destination operation is not a valid operation on the destination object.</li>
* </ol>
*/
public TransportOrder createTransportOrder(TransportOrderCreationTO to)
throws ObjectUnknownException,
ObjectExistsException,
IllegalArgumentException {
TransportOrder newOrder = new TransportOrder(
nameFor(to),
toDriveOrders(to.getDestinations())
)
.withCreationTime(Instant.now())
.withPeripheralReservationToken(to.getPeripheralReservationToken())
.withIntendedVehicle(toVehicleReference(to.getIntendedVehicleName()))
.withType(to.getType())
.withDeadline(to.getDeadline())
.withDispensable(to.isDispensable())
.withWrappingSequence(getWrappingSequence(to))
.withDependencies(getDependencies(to))
.withProperties(to.getProperties());
LOG.info(
"Transport order is being created: {} -- {}",
newOrder.getName(),
newOrder.getAllDriveOrders()
);
getObjectRepo().addObject(newOrder);
emitObjectEvent(newOrder, null, TCSObjectEvent.Type.OBJECT_CREATED);
if (newOrder.getWrappingSequence() != null) {
OrderSequence sequence = getObjectRepo().getObject(
OrderSequence.class,
newOrder.getWrappingSequence()
);
OrderSequence prevSeq = sequence;
sequence = sequence.withOrder(newOrder.getReference());
getObjectRepo().replaceObject(sequence);
emitObjectEvent(sequence, prevSeq, TCSObjectEvent.Type.OBJECT_MODIFIED);
}
// Return the newly created transport order.
return newOrder;
}
/**
* Sets a transport order's state.
*
* @param ref A reference to the transport order to be modified.
* @param newState The transport order's new state.
* @return The modified transport order.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
*/
public TransportOrder setTransportOrderState(
TCSObjectReference<TransportOrder> ref,
TransportOrder.State newState
)
throws ObjectUnknownException {
TransportOrder previousState = getObjectRepo().getObject(TransportOrder.class, ref);
checkArgument(
!previousState.getState().isFinalState(),
"Transport order %s already in a final state, not changing %s -> %s.",
ref.getName(),
previousState.getState(),
newState
);
LOG.info(
"Transport order's state changes: {} -- {} -> {}",
previousState.getName(),
previousState.getState(),
newState
);
TransportOrder order = previousState.withState(newState);
getObjectRepo().replaceObject(order);
emitObjectEvent(
order,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return order;
}
/**
* Sets a transport order's processing vehicle.
*
* @param orderRef A reference to the transport order to be modified.
* @param vehicleRef A reference to the vehicle processing the order.
* @param driveOrders The drive orders containing the data to be copied into this transport
* order's drive orders.
* @return The modified transport order.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
* @throws IllegalArgumentException If the destinations of the given drive
* orders do not match the destinations of the drive orders in this transport
* order.
*/
public TransportOrder setTransportOrderProcessingVehicle(
TCSObjectReference<TransportOrder> orderRef,
TCSObjectReference<Vehicle> vehicleRef,
List<DriveOrder> driveOrders
)
throws ObjectUnknownException,
IllegalArgumentException {
TransportOrder order = getObjectRepo().getObject(TransportOrder.class, orderRef);
LOG.info(
"Transport order's processing vehicle changes: {} -- {} -> {}",
order.getName(),
toObjectName(order.getProcessingVehicle()),
toObjectName(vehicleRef)
);
TransportOrder previousState = order;
if (vehicleRef == null) {
order = order.withProcessingVehicle(null);
getObjectRepo().replaceObject(order);
}
else {
Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleRef);
order = order.withProcessingVehicle(vehicle.getReference())
.withDriveOrders(driveOrders)
.withCurrentDriveOrderIndex(0);
getObjectRepo().replaceObject(order);
if (order.getCurrentDriveOrder() != null) {
order = order.withCurrentDriveOrderState(DriveOrder.State.TRAVELLING);
getObjectRepo().replaceObject(order);
}
}
emitObjectEvent(
order,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return order;
}
/**
* Copies drive order data from a list of drive orders to the given transport
* order's future drive orders.
*
* @param orderRef A reference to the transport order to be modified.
* @param newOrders The drive orders containing the data to be copied into
* this transport order's drive orders.
* @return The modified transport order.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
* @throws IllegalArgumentException If the destinations of the given drive
* orders do not match the destinations of the drive orders in this transport
* order.
*/
public TransportOrder setTransportOrderDriveOrders(
TCSObjectReference<TransportOrder> orderRef,
List<DriveOrder> newOrders
)
throws ObjectUnknownException,
IllegalArgumentException {
TransportOrder previousState = getObjectRepo().getObject(TransportOrder.class, orderRef);
LOG.debug(
"Transport order's drive orders change: {} -- {} -> {}",
previousState.getName(),
previousState.getAllDriveOrders(),
newOrders
);
TransportOrder order = previousState.withDriveOrders(newOrders);
getObjectRepo().replaceObject(order);
emitObjectEvent(
order,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return order;
}
/**
* Updates a transport order's current drive order.
* Marks the current drive order as finished, adds it to the list of past
* drive orders and sets the current drive order to the next one of the list
* of future drive orders (or <code>null</code>, if that list is empty).
* If the current drive order is <code>null</code> because all drive orders
* have been finished already or none has been started, yet, nothing happens.
*
* @param ref A reference to the transport order to be modified.
* @return The modified transport order.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
*/
public TransportOrder setTransportOrderNextDriveOrder(TCSObjectReference<TransportOrder> ref)
throws ObjectUnknownException {
TransportOrder previousState = getObjectRepo().getObject(TransportOrder.class, ref);
TransportOrder order = previousState;
// First, mark the current drive order as FINISHED and send an event.
// Then, shift drive orders and send a second event.
// Then, mark the current drive order as TRAVELLING and send another event.
if (order.getCurrentDriveOrder() != null) {
LOG.info(
"Transport order's drive order finished: {} -- {}",
order.getName(),
order.getCurrentDriveOrder().getDestination()
);
order = order.withCurrentDriveOrderState(DriveOrder.State.FINISHED);
getObjectRepo().replaceObject(order);
TransportOrder newState = order;
emitObjectEvent(
newState,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
previousState = newState;
order = order.withCurrentDriveOrderIndex(order.getCurrentDriveOrderIndex() + 1)
.withCurrentRouteStepIndex(TransportOrder.ROUTE_STEP_INDEX_DEFAULT);
getObjectRepo().replaceObject(order);
newState = order;
emitObjectEvent(
newState,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
previousState = newState;
if (order.getCurrentDriveOrder() != null) {
order = order.withCurrentDriveOrderState(DriveOrder.State.TRAVELLING);
getObjectRepo().replaceObject(order);
newState = order;
emitObjectEvent(
newState,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
previousState = newState;
}
}
emitObjectEvent(
order,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return order;
}
/**
* Sets a transport order's index of the last route step travelled for the currently processed
* drive order.
*
* @param ref A reference to the transport order to be modified.
* @param index The new index.
* @return The modified transport order.
* @throws ObjectUnknownException If the referenced transport order does not exist.
*/
public TransportOrder setTransportOrderCurrentRouteStepIndex(
TCSObjectReference<TransportOrder> ref,
int index
)
throws ObjectUnknownException {
TransportOrder previousState = getObjectRepo().getObject(TransportOrder.class, ref);
LOG.debug(
"Transport order's route step index changes: {} -- {} -> {}",
previousState.getName(),
previousState.getCurrentRouteStepIndex(),
index
);
TransportOrder order = previousState.withCurrentRouteStepIndex(index);
getObjectRepo().replaceObject(order);
emitObjectEvent(
order,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return order;
}
/**
* Set a transport order's intended vehicle.
*
* @param orderRef A reference to the transport order to be modified.
* @param vehicleRef A reference to the vehicle intended for the transport order.
* @return The modified transport order.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool or if the intended vehicle is not null and not in this this pool.
* @throws IllegalArgumentException If the transport order is not in the dispatchable state.
*/
public TransportOrder setTransportOrderIntendedVehicle(
TCSObjectReference<TransportOrder> orderRef,
TCSObjectReference<Vehicle> vehicleRef
)
throws ObjectUnknownException,
IllegalArgumentException {
TransportOrder order = getObjectRepo().getObject(TransportOrder.class, orderRef);
if (!canSetIntendedVehicle(order)) {
throw new IllegalArgumentException(
String.format(
"Cannot set intended vehicle '%s' for transport order '%s' in state '%s'",
toObjectName(vehicleRef),
order.getName(),
order.getState()
)
);
}
if (vehicleRef != null && getObjectRepo().getObjectOrNull(Vehicle.class, vehicleRef) == null) {
throw new ObjectUnknownException("Unknown vehicle: " + vehicleRef.getName());
}
LOG.info(
"Transport order's intended vehicle changes: {} -- {} -> {}",
order.getName(),
toObjectName(order.getIntendedVehicle()),
toObjectName(vehicleRef)
);
TransportOrder previousState = order;
order = order.withIntendedVehicle(vehicleRef);
getObjectRepo().replaceObject(order);
emitObjectEvent(
order,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return order;
}
/**
* Removes the referenced transport order from this pool.
*
* @param ref A reference to the transport order to be removed.
* @return The removed transport order.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
*/
public TransportOrder removeTransportOrder(TCSObjectReference<TransportOrder> ref)
throws ObjectUnknownException {
TransportOrder order = getObjectRepo().getObject(TransportOrder.class, ref);
// Make sure only orders in a final state are removed.
checkArgument(
order.getState().isFinalState(),
"Transport order %s is not in a final state.",
order.getName()
);
getObjectRepo().removeObject(ref);
emitObjectEvent(
null,
order,
TCSObjectEvent.Type.OBJECT_REMOVED
);
return order;
}
/**
* Adds a new order sequence to the pool.
*
* @param to The transfer object from which to create the new order sequence.
* @return The newly created order sequence.
* @throws ObjectExistsException If an object with the new object's name already exists.
* @throws ObjectUnknownException If any object referenced in the TO does not exist.
*/
public OrderSequence createOrderSequence(OrderSequenceCreationTO to)
throws ObjectExistsException,
ObjectUnknownException {
OrderSequence newSequence = new OrderSequence(nameFor(to))
.withType(to.getType())
.withIntendedVehicle(toVehicleReference(to.getIntendedVehicleName()))
.withFailureFatal(to.isFailureFatal())
.withProperties(to.getProperties());
LOG.info("Order sequence is being created: {}", newSequence.getName());
getObjectRepo().addObject(newSequence);
emitObjectEvent(
newSequence,
null,
TCSObjectEvent.Type.OBJECT_CREATED
);
// Return the newly created transport order.
return newSequence;
}
/**
* Sets an order sequence's finished index.
*
* @param seqRef A reference to the order sequence to be modified.
* @param index The sequence's new finished index.
* @return The modified order sequence.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
*/
public OrderSequence setOrderSequenceFinishedIndex(
TCSObjectReference<OrderSequence> seqRef,
int index
)
throws ObjectUnknownException {
OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, seqRef);
LOG.debug(
"Order sequence's finished index changes: {} -- {} -> {}",
previousState.getName(),
previousState.getFinishedIndex(),
index
);
OrderSequence sequence = previousState.withFinishedIndex(index);
getObjectRepo().replaceObject(sequence);
emitObjectEvent(
sequence,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return sequence;
}
/**
* Sets an order sequence's complete flag.
*
* @param seqRef A reference to the order sequence to be modified.
* @return The modified order sequence.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
*/
public OrderSequence setOrderSequenceComplete(TCSObjectReference<OrderSequence> seqRef)
throws ObjectUnknownException {
OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, seqRef);
LOG.info("Order sequence being marked as complete: {}", previousState.getName());
OrderSequence sequence = previousState.withComplete(true);
getObjectRepo().replaceObject(sequence);
emitObjectEvent(
sequence,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return sequence;
}
/**
* Sets an order sequence's finished flag.
*
* @param seqRef A reference to the order sequence to be modified.
* @return The modified order sequence.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
*/
public OrderSequence setOrderSequenceFinished(TCSObjectReference<OrderSequence> seqRef)
throws ObjectUnknownException {
OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, seqRef);
LOG.info("Order sequence being marked as finished: {}", previousState.getName());
OrderSequence sequence = previousState.withFinished(true);
getObjectRepo().replaceObject(sequence);
emitObjectEvent(
sequence,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return sequence;
}
/**
* Sets an order sequence's processing vehicle.
*
* @param seqRef A reference to the order sequence to be modified.
* @param vehicleRef A reference to the vehicle processing the order sequence.
* @return The modified order sequence.
* @throws ObjectUnknownException If the referenced transport order is not
* in this pool.
*/
public OrderSequence setOrderSequenceProcessingVehicle(
TCSObjectReference<OrderSequence> seqRef,
TCSObjectReference<Vehicle> vehicleRef
)
throws ObjectUnknownException {
OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, seqRef);
LOG.info(
"Order sequence's processing vehicle changes: {} -- {} -> {}",
previousState.getName(),
toObjectName(previousState.getProcessingVehicle()),
toObjectName(vehicleRef)
);
OrderSequence sequence = previousState;
if (vehicleRef == null) {
sequence = sequence.withProcessingVehicle(null);
getObjectRepo().replaceObject(sequence);
}
else {
Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleRef);
sequence = sequence.withProcessingVehicle(vehicle.getReference());
getObjectRepo().replaceObject(sequence);
}
emitObjectEvent(
sequence,
previousState,
TCSObjectEvent.Type.OBJECT_MODIFIED
);
return sequence;
}
/**
* Removes the referenced order sequence from this pool.
*
* @param ref A reference to the order sequence to be removed.
* @return The removed order sequence.
* @throws ObjectUnknownException If the referenced order sequence is not in this pool.
*/
public OrderSequence removeOrderSequence(TCSObjectReference<OrderSequence> ref)
throws ObjectUnknownException {
OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, ref);
OrderSequence sequence = previousState;
// XXX Any sanity checks here?
getObjectRepo().removeObject(ref);
emitObjectEvent(
null,
previousState,
TCSObjectEvent.Type.OBJECT_REMOVED
);
return sequence;
}
/**
* Removes a completed order sequence including its transport orders.
*
* @param ref A reference to the order sequence.
* @throws ObjectUnknownException If the referenced order sequence is not in this pool.
* @throws IllegalArgumentException If the order sequence is not finished, yet.
*/
public void removeFinishedOrderSequenceAndOrders(TCSObjectReference<OrderSequence> ref)
throws ObjectUnknownException,
IllegalArgumentException {
OrderSequence previousState = getObjectRepo().getObject(OrderSequence.class, ref);
checkArgument(
previousState.isFinished(),
"Order sequence %s is not finished",
previousState.getName()
);
OrderSequence sequence = previousState;
getObjectRepo().removeObject(ref);
emitObjectEvent(null, previousState, TCSObjectEvent.Type.OBJECT_REMOVED);
// Also remove all orders in the sequence.
for (TCSObjectReference<TransportOrder> orderRef : sequence.getOrders()) {
removeTransportOrder(orderRef);
}
}
private Set<TCSObjectReference<TransportOrder>> getDependencies(TransportOrderCreationTO to)
throws ObjectUnknownException {
Set<TCSObjectReference<TransportOrder>> result = new HashSet<>();
for (String dependencyName : to.getDependencyNames()) {
result.add(getObjectRepo().getObject(TransportOrder.class, dependencyName).getReference());
}
return result;
}
private TCSObjectReference<OrderSequence> getWrappingSequence(TransportOrderCreationTO to)
throws ObjectUnknownException,
IllegalArgumentException {
if (to.getWrappingSequence() == null) {
return null;
}
OrderSequence sequence = getObjectRepo().getObject(
OrderSequence.class,
to.getWrappingSequence()
);
checkArgument(!sequence.isComplete(), "Order sequence %s is already complete", sequence);
checkArgument(
Objects.equals(to.getType(), sequence.getType()),
"Order sequence %s has different type than order %s: %s != %s",
sequence,
to.getName(),
sequence.getType(),
to.getType()
);
checkArgument(
Objects.equals(to.getIntendedVehicleName(), getIntendedVehicleName(sequence)),
"Order sequence %s has different intended vehicle than order %s: %s != %s",
sequence,
to.getName(),
sequence.getIntendedVehicle(),
to.getIntendedVehicleName()
);
return sequence.getReference();
}
private TCSObjectReference<Vehicle> toVehicleReference(String vehicleName)
throws ObjectUnknownException {
if (vehicleName == null) {
return null;
}
Vehicle vehicle = getObjectRepo().getObject(Vehicle.class, vehicleName);
return vehicle.getReference();
}
private List<DriveOrder> toDriveOrders(List<DestinationCreationTO> dests)
throws ObjectUnknownException,
IllegalArgumentException {
List<DriveOrder> result = new ArrayList<>(dests.size());
for (DestinationCreationTO destTo : dests) {
TCSObject<?> destObject = getObjectRepo().getObjectOrNull(destTo.getDestLocationName());
if (destObject instanceof Point) {
if (!isValidOperationOnPoint(destTo.getDestOperation())) {
throw new IllegalArgumentException(
destTo.getDestOperation()
+ " is not a valid operation for point destination " + destObject.getName()
);
}
}
else if (destObject instanceof Location) {
if (!isValidLocationDestination(destTo, (Location) destObject)) {
throw new IllegalArgumentException(
destTo.getDestOperation()
+ " is not a valid operation for location destination " + destObject.getName()
);
}
}
else {
throw new ObjectUnknownException(destTo.getDestLocationName());
}
result.add(
new DriveOrder(
new DriveOrder.Destination(destObject.getReference())
.withOperation(destTo.getDestOperation())
.withProperties(destTo.getProperties())
)
);
}
return result;
}
private boolean isValidOperationOnPoint(String operation) {
return operation.equals(Destination.OP_MOVE)
|| operation.equals(Destination.OP_PARK);
}
private boolean isValidLocationDestination(DestinationCreationTO dest, Location location) {
LocationType type = getObjectRepo()
.getObjectOrNull(LocationType.class, location.getType().getName());
return type != null
&& isValidOperationOnLocationType(dest.getDestOperation(), type)
&& location.getAttachedLinks().stream()
.anyMatch(link -> isValidOperationOnLink(dest.getDestOperation(), link));
}
private boolean isValidOperationOnLink(String operation, Link link) {
return link.getAllowedOperations().isEmpty()
|| link.getAllowedOperations().contains(operation)
|| operation.equals(Destination.OP_NOP);
}
private boolean isValidOperationOnLocationType(String operation, LocationType type) {
return type.isAllowedOperation(operation)
|| operation.equals(Destination.OP_NOP);
}
@Nullable
private String getIntendedVehicleName(OrderSequence sequence) {
return sequence.getIntendedVehicle() == null ? null : sequence.getIntendedVehicle().getName();
}
@Nonnull
private String nameFor(
@Nonnull
TransportOrderCreationTO to
) {
if (to.hasIncompleteName()) {
return objectNameProvider.apply(to);
}
else {
return to.getName();
}
}
@Nonnull
private String nameFor(
@Nonnull
OrderSequenceCreationTO to
) {
if (to.hasIncompleteName()) {
return objectNameProvider.apply(to);
}
else {
return to.getName();
}
}
@Nonnull
private String toObjectName(TCSObjectReference<?> ref) {
return ref == null ? "null" : ref.getName();
}
private boolean canSetIntendedVehicle(TransportOrder order) {
return order.hasState(TransportOrder.State.RAW)
|| order.hasState(TransportOrder.State.ACTIVE)
|| order.hasState(TransportOrder.State.DISPATCHABLE);
}
}

Some files were not shown because too many files have changed in this diff Show More