feat: simulate wcs traffic control
This commit is contained in:
@@ -60,6 +60,33 @@ public final class DispatchQueueEntry {
|
||||
);
|
||||
}
|
||||
|
||||
public static DispatchQueueEntry avoidance(
|
||||
String queueId,
|
||||
String vehicleId,
|
||||
String startSite,
|
||||
String endSite
|
||||
) {
|
||||
if (queueId == null || queueId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("queue id must not be blank");
|
||||
}
|
||||
if (vehicleId == null || vehicleId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("vehicle id must not be blank");
|
||||
}
|
||||
if (startSite == null || startSite.trim().isEmpty()
|
||||
|| endSite == null || endSite.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("avoidance sites must not be blank");
|
||||
}
|
||||
return new DispatchQueueEntry(
|
||||
queueId,
|
||||
queueId,
|
||||
vehicleId,
|
||||
QueueType.AVOIDANCE,
|
||||
startSite,
|
||||
endSite,
|
||||
9
|
||||
);
|
||||
}
|
||||
|
||||
public void markIssued(List<String> issuedPath) {
|
||||
if (issued != 0) {
|
||||
throw new IllegalStateException("queue has already been issued");
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ft546.wcs.demo.traffic;
|
||||
|
||||
import com.ft546.wcs.demo.model.Site;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class GridMap {
|
||||
private final Map<String, Site> sites = new LinkedHashMap<String, Site>();
|
||||
private final Map<String, Set<String>> neighbors =
|
||||
new LinkedHashMap<String, Set<String>>();
|
||||
private final Set<String> blockedSites = new LinkedHashSet<String>();
|
||||
private final Map<String, String> cacheSitesByChannel =
|
||||
new LinkedHashMap<String, String>();
|
||||
|
||||
public void addSite(Site site) {
|
||||
if (site == null) {
|
||||
throw new IllegalArgumentException("site must not be null");
|
||||
}
|
||||
if (sites.containsKey(site.getId())) {
|
||||
throw new IllegalArgumentException("duplicate site: " + site.getId());
|
||||
}
|
||||
sites.put(site.getId(), site);
|
||||
neighbors.put(site.getId(), new LinkedHashSet<String>());
|
||||
}
|
||||
|
||||
public void connect(String left, String right) {
|
||||
requireSite(left);
|
||||
requireSite(right);
|
||||
neighbors.get(left).add(right);
|
||||
neighbors.get(right).add(left);
|
||||
}
|
||||
|
||||
public void block(String siteId) {
|
||||
requireSite(siteId);
|
||||
blockedSites.add(siteId);
|
||||
}
|
||||
|
||||
public void setCacheSite(String channel, String siteId) {
|
||||
if (channel == null || channel.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("channel must not be blank");
|
||||
}
|
||||
requireSite(siteId);
|
||||
cacheSitesByChannel.put(channel, siteId);
|
||||
}
|
||||
|
||||
public Site getSite(String siteId) {
|
||||
return sites.get(siteId);
|
||||
}
|
||||
|
||||
public Map<String, Site> getSites() {
|
||||
return Collections.unmodifiableMap(sites);
|
||||
}
|
||||
|
||||
public List<String> getNeighbors(String siteId) {
|
||||
requireSite(siteId);
|
||||
List<String> result = new ArrayList<String>(neighbors.get(siteId));
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Set<String> getBlockedSites() {
|
||||
return Collections.unmodifiableSet(blockedSites);
|
||||
}
|
||||
|
||||
public String getCacheSite(String channel) {
|
||||
return cacheSitesByChannel.get(channel);
|
||||
}
|
||||
|
||||
private void requireSite(String siteId) {
|
||||
if (!sites.containsKey(siteId)) {
|
||||
throw new IllegalArgumentException("unknown site: " + siteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ft546.wcs.demo.traffic;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class GridPathPlanner {
|
||||
|
||||
public List<String> shortestPath(
|
||||
GridMap map,
|
||||
String start,
|
||||
String end,
|
||||
Set<String> dynamicBlocked
|
||||
) {
|
||||
if (map == null || map.getSite(start) == null || map.getSite(end) == null) {
|
||||
throw new IllegalArgumentException("map, start and end sites are required");
|
||||
}
|
||||
|
||||
Set<String> blocked = new LinkedHashSet<String>(map.getBlockedSites());
|
||||
if (dynamicBlocked != null) {
|
||||
blocked.addAll(dynamicBlocked);
|
||||
}
|
||||
blocked.remove(start);
|
||||
if (blocked.contains(end)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Deque<String> open = new ArrayDeque<String>();
|
||||
Set<String> visited = new HashSet<String>();
|
||||
Map<String, String> parent = new HashMap<String, String>();
|
||||
open.add(start);
|
||||
visited.add(start);
|
||||
|
||||
while (!open.isEmpty()) {
|
||||
String current = open.removeFirst();
|
||||
if (current.equals(end)) {
|
||||
return buildPath(parent, end);
|
||||
}
|
||||
for (String neighbor : map.getNeighbors(current)) {
|
||||
if (!blocked.contains(neighbor) && visited.add(neighbor)) {
|
||||
parent.put(neighbor, current);
|
||||
open.addLast(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<String> buildPath(Map<String, String> parent, String end) {
|
||||
List<String> reversed = new ArrayList<String>();
|
||||
String current = end;
|
||||
while (current != null) {
|
||||
reversed.add(current);
|
||||
current = parent.get(current);
|
||||
}
|
||||
Collections.reverse(reversed);
|
||||
return Collections.unmodifiableList(reversed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.ft546.wcs.demo.traffic;
|
||||
|
||||
import com.ft546.wcs.demo.model.DecisionCode;
|
||||
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||
import com.ft546.wcs.demo.model.Site;
|
||||
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class TrafficController {
|
||||
private final GridMap map;
|
||||
private final GridPathPlanner planner;
|
||||
|
||||
public TrafficController(GridMap map, GridPathPlanner planner) {
|
||||
if (map == null || planner == null) {
|
||||
throw new IllegalArgumentException("map and planner are required");
|
||||
}
|
||||
this.map = map;
|
||||
this.planner = planner;
|
||||
}
|
||||
|
||||
public TrafficDecision plan(
|
||||
VehicleSnapshot vehicle,
|
||||
DispatchQueueEntry queue,
|
||||
Collection<VehicleSnapshot> vehicles,
|
||||
Collection<DispatchQueueEntry> queues
|
||||
) {
|
||||
requireInputs(vehicle, queue, vehicles, queues);
|
||||
Site start = map.getSite(vehicle.getCurrentSite());
|
||||
Site target = map.getSite(queue.getEndSite());
|
||||
if (start == null || target == null) {
|
||||
return TrafficDecision.hold(
|
||||
DecisionCode.REJECT_INVALID_SITE,
|
||||
"unknown start or target site"
|
||||
);
|
||||
}
|
||||
|
||||
VehicleSnapshot channelBlocker =
|
||||
findChannelBlocker(vehicle, target, vehicles);
|
||||
if (channelBlocker != null) {
|
||||
if (hasActiveQueue(channelBlocker.getVehicleId(), queues)) {
|
||||
return TrafficDecision.hold(
|
||||
DecisionCode.HOLD_CHANNEL_BLOCKED,
|
||||
"channel " + target.getChannel() + " is occupied by vehicle "
|
||||
+ channelBlocker.getVehicleId()
|
||||
);
|
||||
}
|
||||
String cacheSite = map.getCacheSite(target.getChannel());
|
||||
if (cacheSite == null) {
|
||||
return TrafficDecision.hold(
|
||||
DecisionCode.HOLD_CHANNEL_BLOCKED,
|
||||
"channel is blocked and has no configured cache site"
|
||||
);
|
||||
}
|
||||
DispatchQueueEntry avoidance = DispatchQueueEntry.avoidance(
|
||||
"AVOID-" + channelBlocker.getVehicleId() + "-FOR-" + queue.getQueueId(),
|
||||
channelBlocker.getVehicleId(),
|
||||
channelBlocker.getCurrentSite(),
|
||||
cacheSite
|
||||
);
|
||||
return TrafficDecision.avoidance(
|
||||
avoidance,
|
||||
"move vehicle " + channelBlocker.getVehicleId()
|
||||
+ " to cache site " + cacheSite
|
||||
);
|
||||
}
|
||||
|
||||
Set<String> blocked = buildBlockedSites(vehicle, vehicles, queues, queue);
|
||||
List<String> path = planner.shortestPath(
|
||||
map,
|
||||
vehicle.getCurrentSite(),
|
||||
queue.getEndSite(),
|
||||
blocked
|
||||
);
|
||||
if (path.isEmpty()) {
|
||||
return TrafficDecision.hold(
|
||||
DecisionCode.HOLD_NO_PATH,
|
||||
"no path from " + vehicle.getCurrentSite() + " to " + queue.getEndSite()
|
||||
);
|
||||
}
|
||||
return TrafficDecision.path(path);
|
||||
}
|
||||
|
||||
private Set<String> buildBlockedSites(
|
||||
VehicleSnapshot vehicle,
|
||||
Collection<VehicleSnapshot> vehicles,
|
||||
Collection<DispatchQueueEntry> queues,
|
||||
DispatchQueueEntry currentQueue
|
||||
) {
|
||||
Set<String> blocked = new LinkedHashSet<String>();
|
||||
for (VehicleSnapshot other : vehicles) {
|
||||
if (!other.getVehicleId().equals(vehicle.getVehicleId())
|
||||
&& !other.isConnected()) {
|
||||
blocked.add(other.getCurrentSite());
|
||||
}
|
||||
}
|
||||
for (DispatchQueueEntry otherQueue : queues) {
|
||||
if (otherQueue.getQueueId().equals(currentQueue.getQueueId())
|
||||
|| otherQueue.getVehicleId().equals(vehicle.getVehicleId())
|
||||
|| otherQueue.getPath().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
VehicleSnapshot owner = findVehicle(otherQueue.getVehicleId(), vehicles);
|
||||
int startIndex = owner == null
|
||||
? 0
|
||||
: otherQueue.getPath().indexOf(owner.getCurrentSite());
|
||||
if (startIndex < 0) {
|
||||
startIndex = 0;
|
||||
}
|
||||
blocked.addAll(
|
||||
otherQueue.getPath().subList(startIndex, otherQueue.getPath().size())
|
||||
);
|
||||
}
|
||||
return blocked;
|
||||
}
|
||||
|
||||
private VehicleSnapshot findChannelBlocker(
|
||||
VehicleSnapshot vehicle,
|
||||
Site target,
|
||||
Collection<VehicleSnapshot> vehicles
|
||||
) {
|
||||
for (VehicleSnapshot other : vehicles) {
|
||||
if (other.getVehicleId().equals(vehicle.getVehicleId())
|
||||
|| !other.isConnected()
|
||||
|| other.getFloor() != target.getFloor()
|
||||
|| other.getWorkMode() != 5
|
||||
|| other.getStatus() != 1) {
|
||||
continue;
|
||||
}
|
||||
Site otherSite = map.getSite(other.getCurrentSite());
|
||||
if (otherSite != null
|
||||
&& target.getChannel().equals(otherSite.getChannel())
|
||||
&& !otherSite.getId().equals(vehicle.getCurrentSite())) {
|
||||
return other;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasActiveQueue(
|
||||
String vehicleId,
|
||||
Collection<DispatchQueueEntry> queues
|
||||
) {
|
||||
for (DispatchQueueEntry queue : queues) {
|
||||
if (vehicleId.equals(queue.getVehicleId())
|
||||
&& queue.getExecutionStatus() != 4) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private VehicleSnapshot findVehicle(
|
||||
String vehicleId,
|
||||
Collection<VehicleSnapshot> vehicles
|
||||
) {
|
||||
for (VehicleSnapshot vehicle : vehicles) {
|
||||
if (vehicleId.equals(vehicle.getVehicleId())) {
|
||||
return vehicle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void requireInputs(
|
||||
VehicleSnapshot vehicle,
|
||||
DispatchQueueEntry queue,
|
||||
Collection<VehicleSnapshot> vehicles,
|
||||
Collection<DispatchQueueEntry> queues
|
||||
) {
|
||||
if (vehicle == null || queue == null || vehicles == null || queues == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"vehicle, queue, vehicles and queues are required"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ft546.wcs.demo.traffic;
|
||||
|
||||
import com.ft546.wcs.demo.model.DecisionCode;
|
||||
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class TrafficDecision {
|
||||
private final DecisionCode code;
|
||||
private final List<String> path;
|
||||
private final DispatchQueueEntry avoidanceQueue;
|
||||
private final String reason;
|
||||
|
||||
private TrafficDecision(
|
||||
DecisionCode code,
|
||||
List<String> path,
|
||||
DispatchQueueEntry avoidanceQueue,
|
||||
String reason
|
||||
) {
|
||||
this.code = code;
|
||||
this.path = Collections.unmodifiableList(new ArrayList<String>(path));
|
||||
this.avoidanceQueue = avoidanceQueue;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public static TrafficDecision path(List<String> path) {
|
||||
return new TrafficDecision(
|
||||
DecisionCode.PATH_READY,
|
||||
path,
|
||||
null,
|
||||
"path ready: " + path
|
||||
);
|
||||
}
|
||||
|
||||
public static TrafficDecision hold(DecisionCode code, String reason) {
|
||||
return new TrafficDecision(code, Collections.<String>emptyList(), null, reason);
|
||||
}
|
||||
|
||||
public static TrafficDecision avoidance(
|
||||
DispatchQueueEntry avoidanceQueue,
|
||||
String reason
|
||||
) {
|
||||
return new TrafficDecision(
|
||||
DecisionCode.AVOIDANCE_CREATED,
|
||||
Collections.<String>emptyList(),
|
||||
avoidanceQueue,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
public DecisionCode getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public List<String> getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public Optional<DispatchQueueEntry> getAvoidanceQueue() {
|
||||
return Optional.ofNullable(avoidanceQueue);
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.ft546.wcs.demo;
|
||||
|
||||
import com.ft546.wcs.demo.model.DecisionCode;
|
||||
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||
import com.ft546.wcs.demo.model.DispatchTask;
|
||||
import com.ft546.wcs.demo.model.Site;
|
||||
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||
import com.ft546.wcs.demo.traffic.GridMap;
|
||||
import com.ft546.wcs.demo.traffic.GridPathPlanner;
|
||||
import com.ft546.wcs.demo.traffic.TrafficController;
|
||||
import com.ft546.wcs.demo.traffic.TrafficDecision;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class TrafficControllerTest {
|
||||
|
||||
@Test
|
||||
void plannerUsesDeterministicAlternateRouteAroundABlockedSite() {
|
||||
GridMap map = diamondMap();
|
||||
GridPathPlanner planner = new GridPathPlanner();
|
||||
|
||||
List<String> path = planner.shortestPath(
|
||||
map,
|
||||
"A",
|
||||
"C",
|
||||
Collections.singleton("B")
|
||||
);
|
||||
|
||||
assertEquals(Arrays.asList("A", "D", "C"), path);
|
||||
}
|
||||
|
||||
@Test
|
||||
void otherQueueRemainingPathAndOfflineVehicleSitesAreLocked() {
|
||||
GridMap map = diamondMap();
|
||||
TrafficController controller = controller(map);
|
||||
VehicleSnapshot car1 = vehicle("1", "A", true);
|
||||
VehicleSnapshot car2 = vehicle("2", "B", true);
|
||||
VehicleSnapshot offlineCar = vehicle("3", "D", false);
|
||||
DispatchQueueEntry car1Queue = businessQueue("TASK-1", "1", "A", "C");
|
||||
DispatchQueueEntry car2Queue = businessQueue("TASK-2", "2", "B", "C");
|
||||
car2Queue.markIssued(Arrays.asList("B", "C"));
|
||||
|
||||
TrafficDecision decision = controller.plan(
|
||||
car1,
|
||||
car1Queue,
|
||||
Arrays.asList(car1, car2, offlineCar),
|
||||
Arrays.asList(car1Queue, car2Queue)
|
||||
);
|
||||
|
||||
assertEquals(DecisionCode.HOLD_NO_PATH, decision.getCode());
|
||||
assertTrue(decision.getReason().contains("no path"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameChannelBlockerCreatesOnePriorityNineAvoidanceQueue() {
|
||||
GridMap map = channelMap();
|
||||
TrafficController controller = controller(map);
|
||||
VehicleSnapshot car1 = vehicle("1", "A", true);
|
||||
VehicleSnapshot blocker = vehicle("2", "B", true);
|
||||
DispatchQueueEntry car1Queue = businessQueue("TASK-1", "1", "A", "TARGET");
|
||||
|
||||
TrafficDecision decision = controller.plan(
|
||||
car1,
|
||||
car1Queue,
|
||||
Arrays.asList(car1, blocker),
|
||||
Collections.singletonList(car1Queue)
|
||||
);
|
||||
|
||||
assertEquals(DecisionCode.AVOIDANCE_CREATED, decision.getCode());
|
||||
assertTrue(decision.getAvoidanceQueue().isPresent());
|
||||
DispatchQueueEntry avoidance = decision.getAvoidanceQueue().get();
|
||||
assertEquals(DispatchQueueEntry.QueueType.AVOIDANCE, avoidance.getQueueType());
|
||||
assertEquals("2", avoidance.getVehicleId());
|
||||
assertEquals("CACHE", avoidance.getEndSite());
|
||||
assertEquals(9, avoidance.getPriority());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingAvoidanceIsNotDuplicatedAndMovingBlockerReleasesTheChannel() {
|
||||
GridMap map = channelMap();
|
||||
TrafficController controller = controller(map);
|
||||
VehicleSnapshot car1 = vehicle("1", "A", true);
|
||||
VehicleSnapshot blocker = vehicle("2", "B", true);
|
||||
DispatchQueueEntry car1Queue = businessQueue("TASK-1", "1", "A", "TARGET");
|
||||
DispatchQueueEntry avoidance =
|
||||
DispatchQueueEntry.avoidance("AVOID-2", "2", "B", "CACHE");
|
||||
|
||||
TrafficDecision held = controller.plan(
|
||||
car1,
|
||||
car1Queue,
|
||||
Arrays.asList(car1, blocker),
|
||||
Arrays.asList(car1Queue, avoidance)
|
||||
);
|
||||
|
||||
assertEquals(DecisionCode.HOLD_CHANNEL_BLOCKED, held.getCode());
|
||||
assertFalse(held.getAvoidanceQueue().isPresent());
|
||||
|
||||
VehicleSnapshot movedBlocker = vehicle("2", "CACHE", true);
|
||||
TrafficDecision released = controller.plan(
|
||||
car1,
|
||||
car1Queue,
|
||||
Arrays.asList(car1, movedBlocker),
|
||||
Collections.singletonList(car1Queue)
|
||||
);
|
||||
|
||||
assertEquals(DecisionCode.PATH_READY, released.getCode());
|
||||
assertEquals(Arrays.asList("A", "B", "TARGET"), released.getPath());
|
||||
}
|
||||
|
||||
private TrafficController controller(GridMap map) {
|
||||
return new TrafficController(map, new GridPathPlanner());
|
||||
}
|
||||
|
||||
private GridMap diamondMap() {
|
||||
GridMap map = new GridMap();
|
||||
map.addSite(new Site("A", 1, 0, 0, "START"));
|
||||
map.addSite(new Site("B", 1, 1, 0, "UPPER"));
|
||||
map.addSite(new Site("C", 1, 2, 0, "TARGET"));
|
||||
map.addSite(new Site("D", 1, 1, 1, "LOWER"));
|
||||
map.connect("A", "B");
|
||||
map.connect("B", "C");
|
||||
map.connect("A", "D");
|
||||
map.connect("D", "C");
|
||||
return map;
|
||||
}
|
||||
|
||||
private GridMap channelMap() {
|
||||
GridMap map = new GridMap();
|
||||
map.addSite(new Site("A", 1, 0, 0, "START"));
|
||||
map.addSite(new Site("B", 1, 1, 0, "AISLE-1"));
|
||||
map.addSite(new Site("TARGET", 1, 2, 0, "AISLE-1"));
|
||||
map.addSite(new Site("CACHE", 1, 1, 1, "CACHE"));
|
||||
map.connect("A", "B");
|
||||
map.connect("B", "TARGET");
|
||||
map.connect("B", "CACHE");
|
||||
map.setCacheSite("AISLE-1", "CACHE");
|
||||
return map;
|
||||
}
|
||||
|
||||
private DispatchQueueEntry businessQueue(
|
||||
String taskId,
|
||||
String vehicleId,
|
||||
String source,
|
||||
String target
|
||||
) {
|
||||
DispatchTask task = new DispatchTask(
|
||||
taskId,
|
||||
DispatchTask.TaskType.OUTBOUND,
|
||||
source,
|
||||
target,
|
||||
1,
|
||||
2
|
||||
);
|
||||
return DispatchQueueEntry.business(task, vehicleId);
|
||||
}
|
||||
|
||||
private VehicleSnapshot vehicle(String id, String site, boolean connected) {
|
||||
return new VehicleSnapshot(id, 1, site, 5, 1, 80, connected, true, false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user