Skip to content

Commit

Permalink
Add a watchdog task to monitor the state of blocks
Browse files Browse the repository at this point in the history
Periodically check the occupancy state of blocks in the plant model
and publish user notifications in case of overfull blocks.

Co-authored-by: Stefan Walter <[email protected]>
Co-authored-by: Martin Grzenia <[email protected]>
Approved-by: Martin Grzenia <[email protected]>
Merged-by: Stefan Walter <[email protected]>
  • Loading branch information
3 people committed Mar 22, 2024
1 parent a20f3ab commit 8ee3540
Show file tree
Hide file tree
Showing 8 changed files with 565 additions and 1 deletion.
2 changes: 2 additions & 0 deletions openTCS-Documentation/src/docs/release-notes/changelog.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ This change log lists the most relevant changes for past releases in reverse chr
* New features and enhancements:
** The creation of ambiguous peripheral jobs (by kernel clients or via the web API) that have the `completionRequired` flag set to `true` is now prevented.
(In those cases it is unclear what should happen to the job's `relatedTransportOrder` (if any) in case the job fails.)
** Add a watchdog task to the kernel which periodically monitors the state of blocks in the plant model and publishes a user notification in case a block is occupied by more than one vehicle.
(Such a situation is usually caused by manually moving vehicles around and leads to deadlock situations.)
** Update web API specification and implementation to version 1.5.0:
*** When retrieving vehicle information via the web API, include the vehicle's orientation angle.
* Bugs fixed:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
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;
Expand Down Expand Up @@ -160,6 +162,8 @@ protected void configure() {
extensionsBinderOperating();
vehicleCommAdaptersBinder();
peripheralCommAdaptersBinder();

configureWatchdogExtension();
}

@SuppressWarnings("deprecation")
Expand Down Expand Up @@ -306,4 +310,15 @@ private void configureKernelExecutor() {
.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));

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/**
* Copyright (c) The openTCS Authors.
*
* This program is free software and subject to the MIT license. (For details,
* see the licensing information (LICENSE.txt) you should have received with
* this copy of the software.)
*/
package org.opentcs.kernel.extensions.watchdog;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import static java.util.Objects.requireNonNull;
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 javax.inject.Inject;
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 static org.opentcs.data.model.Vehicle.IntegrationLevel.TO_BE_RESPECTED;
import static org.opentcs.data.model.Vehicle.IntegrationLevel.TO_BE_UTILIZED;
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright (c) The openTCS Authors.
*
* This program is free software and subject to the MIT license. (For details,
* see the licensing information (LICENSE.txt) you should have received with
* this copy of the software.)
*/
package org.opentcs.kernel.extensions.watchdog;

import static java.util.Objects.requireNonNull;
import javax.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;

/**
* Creates a new instance.
*
* @param blockCheck The block check task.
*/
@Inject
public Watchdog(BlockConsistencyCheck blockCheck) {
this.blockCheck = requireNonNull(blockCheck, "blockCheck");
}

@Override
public void initialize() {
if (isInitialized()) {
return;
}

blockCheck.initialize();
initialized = true;
}

@Override
public boolean isInitialized() {
return initialized;
}

@Override
public void terminate() {
if (!isInitialized()) {
return;
}

blockCheck.terminate();
initialized = false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) The openTCS Authors.
*
* This program is free software and subject to the MIT license. (For details,
* see the licensing information (LICENSE.txt) you should have received with
* this copy of the software.)
*/
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 which to check for block consistency in milliseconds.",
changesApplied = ConfigurationEntry.ChangesApplied.ON_APPLICATION_START,
orderKey = "1_block")
int blockConsistencyCheckInterval();
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ public void addNotification(UserNotification notification) {
requireNonNull(notification, "notification");

notifications.add(notification);
LOG.debug("New notification added: {}", notification.getText());
LOG.info("User notification added: {}", notification);

// Make sure we don't have too many messages now.
cutBackMessages();
// Emit an event for this message.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ virtualvehicle.vehicleLengthUnloaded = 1000
virtualperipheral.enable = true

statisticscollector.enable = true

watchdog.blockConsistencyCheckInterval = 10000
Loading

0 comments on commit 8ee3540

Please sign in to comment.