org.opentcs.kernel.vehicles.PeripheralInteraction Maven / Gradle / Ivy
// 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.
*
* 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.
*
*
* 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.
*
*
* Once the interaction is finished, an interaction-specific callback is executed.
*
*/
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 vehicleRef;
/**
* A reference to the transport order this interaction is related to.
*/
private final TCSObjectReference 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 operations;
/**
* The jobs that are required to be finished in order for this interaction itself to be marked as
* finished.
*/
private final List 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 vehicleRef,
@Nonnull
TCSObjectReference orderRef,
@Nonnull
MovementCommand movementCommand,
@Nonnull
List 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 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;
}
}