Let’s build a new and more complex Cadence application for Drone Deliveries! This is the first part of a multi-part Cadence Drone series and introduces the Drone Delivery problem, the main Drone Cadence Workflow, and an appendix on Drone movement.
1. The Sky Is Full of Drones (And Ravens)
(Source: Shutterstock)
My recent New Year’s holiday reading was Neal Stephenson’s “Termination Shock”, imagining a time in the near future in which drones are ubiquitous, and curiously features trained drone-destroying birds of prey. Fact is often stranger than fiction, as this reminded me about the trial drone delivery service near the part of Canberra where I live, and which had recently been in the news due to Raven attacks. Unfortunately, when I checked it turns out that they don’t deliver to my suburb yet, even though I have multiple perfect drone delivery landing locations nearby. Oh well, fiction will have to suffice for the time being.
So, for my next Cadence experiment, I decided to build a Drone Delivery Service (simulated, even though I do have several generations worth of discarded drones cluttering up my garage!) It will be modeled on the real local trial, which works by allowing customers to order small items from selected participating shops (e.g. medicines, coffee, food, small parts, etc.). There is a single Drone Base (similar to, but probably more boring than an Evil Villain’s lair, Tracy Island from Thunderbirds “are go”, or the Bees Drone Congregation Area) where all the drones hang out when they aren’t out on a delivery.
The base is where they recharge, and when an order is ready to go, they fly from the base to the shop, pick up the order, then fly to the customer’s location and deliver the order. They then return to base and the process repeats. The drones are autonomous, so there can be lots of them. But there are limits to the weight of goods, the times of operation, where they can and can’t fly (e.g. exclusion zones), many potential failure modes (e.g. failure to reach destinations or deliver orders, crashing, etc.), and maximum distances the drones can operate over in a single charge with sufficient safety margin to return to base (particularly if they have to avoid attacking ravens or get blown off course, etc.). The only factor I decided to take into consideration was the distance and time of travel for the time being.
2. Drone Delivery Application: Cadence Workflow Design
I wanted to keep things simple, but also interesting, for my Drone Delivery Demo. So I decided to implement everything that was stateful and could have actions or tasks as Cadence workflows. There are two Workflow types that interact.
Each Drone is modeled as a workflow, which transitions through various states depending on whether it’s ready at the base, waiting for an order, flying to collect an order, picking it up, flying to deliver it, delivering it, and then returning to base and charging. After each of these delivery cycles, the Drone Workflow starts a new instance (with the same Workflow Id, but a different Run Id), and is ready for the next delivery.
I also decided to model Orders as Cadence Workflow. This is because Orders also have a state and transition from one state to the next (e.g. order creation, ready for pick up, picked up by Drone, on way, delivered, order complete). They may also have actions. For example, the Order Workflow is actually responsible for generating random Order pickup and Delivery locations that are within Drone flying range of the base location, and indicating when the Order is ready for pickup. We’ll look at the Order Workflow in more detail in the next blog.
Here are the top-level Cadence Drone Workflow steps. One Drone Workflow instance is created initially per Drone. The workflow models a single complete delivery of one Order, including drone recharging, and then starts a new instance that is fully charged and ready for the next delivery. Note that this code is just the @WorkflowMethod (entry point) method of the Drone workflow implementation and is incomplete by itself—it uses some private helper functions for some steps and needs the Workflow interface and activities to run. The complete code is available here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
// missing code public static class DroneWorkflowImpl implements DroneWorkflow { // missing code @Override public String startWorkflow(String name) { droneName = name; System.out.println("Started Drone workflow " + name + ", ID=" + Workflow.getWorkflowInfo().getWorkflowId()); // STEP 0 - ready // Drones always start ready, at the base location newStateAndLocation("ready", "base"); // STEP 1 - wait for an Order // this step calls a real activity which blocks until an order arrives // returns an OrderWorkflow which we used to signal Order WF, also sets OrderID which is just a String orderWorkflow = step1_GetOrder(); newStateAndLocation("gotOrder", "base"); // STEP 2 - generate "flight plan" using the order and delivery locations from the Order // The Order WF is responsible for generating random order and delivery locations that are within Drone range step2_GenerateFlightPlan(); newStateAndLocation("flightPlanGenerated", "base"); // STEP 3 - another real activity - flying to get the order System.out.println("Drone + " + name + " flying to pickup Order"); newStateAndLocation("flyingToOrder", "betweenBaseAndOrder"); // Let the Order WF know that the drone is on the way orderWorkflow.signalOrder("droneOnWayForPickup"); // nextLeg is where the Drone movement is calculated, causing the drone to "fly" from planStart to planOrder locations // false and null arguments ensure that the Order location isn't updated yet, but charge is reduced activities.nextLeg(planStart, planOrder, false, null); // STEP 4 - arrived at order location, collect order - this takes time and uses charge to System.out.println("Drone + " + name + " picking up Order"); newStateAndLocation("pickingUpOrder", "orderLocation"); step4_pickUpOrder(); // STEP 5 - flying to deliver the order System.out.println("Drone + " + name + " delivering Order..."); newStateAndLocation("startedDelivery", "betweenOrderAndDelivery"); // next GPS location drone flies to nextGPSLocation = planDelivery; // let Order WF know the delivery has started orderWorkflow.signalOrder("outForDelivery"); orderWorkflow.updateLocation("onWay"); // drone flies to delivery location, updating drone and order locations and drone charge as it goes activities.nextLeg(planOrder, planDelivery, true, orderID); // STEP 6 - drop order System.out.println("Drone + " + name + " dropping Order!"); newStateAndLocation("droppingOrder", "deliveryLocation"); step6_dropOrder(); // Step 7 - return to base System.out.println("Drone + " + name + " returning to Base"); newStateAndLocation("returningToBase", "betweenDeliveryAndBase"); nextGPSLocation = planEnd; // fly back to base, update drone location and charge, but not Order location as it's already been delivered. activities.nextLeg(planDelivery, planEnd, false, null); // STEP 8 - back at base System.out.println("Drone + " + name + " returned to Base!"); newStateAndLocation("backAtBase", "base"); // STEP 9 - check order - if successful then Order WF completes newStateAndLocation("checkOrder", "base"); step9_checkOrder(); // Step 10 - delivery complete newStateAndLocation("droneDeliveryCompleted", "base"); // Step 11 - charge newStateAndLocation("charging", "base"); step11_recharge(); // Step 12 - fully recharged newStateAndLocation("charged", "base"); System.out.println("Starting new Drone delivery WF with coninueAsnew with same WF ID!"); Workflow.continueAsNew(name); return "Drone Delivery Workflow " + name + " completed!"; } } |
3. Cadence Use Case: Drone Delivery From A to B
The above code and this picture makes Drone deliveries from one location to another look easy. In practice, it’s a bit more complex, and motivates my choice of using Cadence Activities for some of the workflow steps.
Location is key for Drone deliveries. I’ve chosen to use the latitude and longitude coordinates (in decimal), with a resolution of 1m. Each drone has a “GPS” that keeps track of the drone’s location, and the Workflow instance for the drone has location state. Drones start at, and (hopefully) return to, the base location. Orders and their corresponding workflow instances, also have location state, including the location of the order pickup, the location where the order is to be delivered to, and finally the actual location of the order, which is updated as the Order is transported by the drone. This is important as the Drone delivery service and shops want to know where the Order is at all times, and also so the customer can track when their delivery is about to arrive so they can collect it from the drone (and save their delivery from angry ravens).
Here’s a map of an example of complete Drone delivery:
But how does a drone move from one location to another? I wrote a simple Drone Maths package to help out with calculations. If you are interested in the details of how we compute drone movement from one location to another, see the Appendix below. The function DroneMaths.nextPosition(start, end, speed, time) computes the next position of the drone given the start location, the end location, and the speed and flight time, and it’s used in the nextLeg() activity.
4. Cadence Workflow Activities
The nextLeg() activity is responsible for simulating the movement of the drone for each leg of the delivery flight and stopping when it arrives at the destination. It updates the Drone location and charge, and optionally the Order location as it “flies”. The activity returns when the drone has arrived. We use a Cadence Activity for this because it potentially runs for a long time, is computationally demanding due to the use of the Drone Maths, so should run in a separate thread to the Drone Workflow itself, and it could potentially fail (as could a real drone, i.e. a non-simulated, real flying drone interacts with the real world, and is also a good example of a non-deterministic activity), in which case we will eventually want to ensure that it restarts and continues the drone movement from where it left of, but we’ll leave that aspect until the next blog when we delve into exception handling in more detail.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
public static class DroneActivitiesImpl implements DroneActivities { // “Fly” from start to end location, return when drone arrives // Update Drone location and charge, and Order Location only if required. public void nextLeg(LatLon start, LatLon end, boolean updateOrderLocation, String orderID) { WorkflowExecution execution = Activity.getWorkflowExecution(); String id = execution.getWorkflowId(); DroneWorkflow droneWF = workflowClient.newWorkflowStub(DroneWorkflow.class, id); OrderWorkflow orderWF = workflowClient.newWorkflowStub(OrderWorkflow.class, orderID); LatLon here = start; while (true) { try { Thread.sleep((int)(moveTime * 1000 * timeScale)); } catch (InterruptedException e) { e.printStackTrace(); return; } LatLon next = DroneMaths.nextPosition(here, end, droneSpeed, moveTime); here = next; System.out.println("Drone flew to new location = " + here.toString()); double distance = DroneMaths.distance(here, end); System.out.println("Distance to destination = " + distance + " km"); // update drone location and charge droneWF.updateGPSLocation(here); droneWF.updateCharge(moveTime); // only update Order location if drone is transporting it if (updateOrderLocation) orderWF.updateGPSLocation(here); // check if we have arrived within 1m if (end.sameLocation(here)) { System.out.println("Drone arrived at destination."); return; } } } } |
You will notice that I’ve used the general purpose Thread.sleep() call in the activity code. This is actually fine, as a Cadence Activity (unlike a Cadence Workflow), can use arbitrary code. However, I did discover one limitation with Activities. It turns out that Activity method arguments and return values must be serializable to a byte array using the provide DataConverer (the default implementation uses a JSON serializer). This is why I had to pass a String OrderID to the method, and construct a new OrderWorkflow instance with it, rather than just pass it a OrderWorkflow (which wasn’t serializable).
Finally, there is one Drone Worklfow step, step1_GetOrder(), that’s critical to progress the Drone workflow. This step waits until an Order is ready for delivery, and sets the Order ID and an OrderWorkflow instance in the main workflow. This helper method is actually a wrapper for another Activity, which blocks until an Order is ready for pickup by a Drone, and guarantees that each ready Order is picked up by exactly one drone. How does this work? It’s actually another example of a Cadence+Kafka integration pattern, and we’ll explain it further in the next blog, and some other Kafka+Cadence patterns (including starting a Cadence Workflow with Kafka, which is how we actually create Order Workflows). In the next blog we’ll also explore the complete solution, including the Order Workflows, and other Cadence features, including queries, retries, heartbeats, continue as new, and side-effects.
P.S. Perhaps the local Drone Delivery company heard about my project? I just received a pamphlet in the mailbox (yes, very old school, but maybe drone delivered?) advertising the start of the service to my suburb—bring on the drone-delivered stuff! (I also discovered that their drones are much faster than mine, I will have to increase the average speed to 100km/h to keep up).
5. Appendix—Drone Movement: Location, Distance, Bearing, Speed, and Charge!
How does a drone move from one location to another? I wrote a simple Drone Maths package to help out with calculations (most of the formulas are covered here). First, we need a function to compute distance (in km) between two lat/lon decimal locations (this also takes into account the curvature of the earth, but for the short distances we’re dealing with the impact of curvature is insignificant—unless the drones are flying very high of course). This is to ensure that the complete delivery (and return) is within the drone’s maximum range capability.
But how do we navigate from one location to another? It turns out that this is just old-time ship-style navigation. For this, we need to know the bearing (in degrees) from one location to another (zero degrees is due North, 90 degrees is due East, 180 degrees is due South, and 270 degrees is due West).
And finally, to work out the next “waypoint” and enable the drone to move on the shortest direct path from one location to another (“as the crow flies”, or “in a beeline”, which is appropriate for drones—we assume there are no obstacles or exclusion zones in the way, including ravens), we have a function to compute the next drone location, nextPosition(). This finds the bearing from the current location to the intended destination location (Order location, delivery location, or base location depending on what state the drone is in), and given the speed (we assume an average drone speed of 20km/hr over the entire journey), calculates the distance traveled in the next time interval (which is configurable, and can be scaled for faster than real-time simulations). It then calls another function to compute the location given the current location, distance covered, and bearing.
If we’ve reached the destination already, we can stop flying and perform the next action (e.g. pickup order, drop order, descend to base). Currently, we assume that each flight path is successful, but for fun, various exceptions (wind, ravens, crashes, etc.) could be introduced with the exceptions handled appropriately (depending on the drone state as well, e.g. if the drone can’t pick up a delivery for some reason, the Order should be rescheduled, perhaps using a priority queue, so that the next available drone is allocated the undelivered Order before accepting other orders).
The nextLeg() activity (see above) is responsible for computing multiple drone movements for each leg of the delivery, but importantly it also sends location updates to the drone and the associated order using Cadence signals, which we saw working in the previous blog. The Drone workflow itself also signals the Order workflow for state changes (e.g. orderWorkflow.signalOrder(“droneHasOrder”)).
Drones are electrical so consume power as they fly and hover, etc. For each incremental distance the drone flies, we reduce the battery charge using a helper function updateCharge(time) which computes the amount of charge used based on the supplied flying time.