bitcraze / crazyflie-firmware

The main firmware for the Crazyflie Nano Quadcopter, Crazyflie Bolt Quadcopter and Roadrunner Positioning Tag.
GNU General Public License v3.0
1.18k stars 1.06k forks source link

Simple onboard collision avoidance for multiple Crazyflies #567

Closed jpreiss closed 3 years ago

jpreiss commented 4 years ago

In multi-quadrotor applications like mapping and search/rescue, where the task requires the robots to spread out, the risk of collisions is inherently low. We want to ensure collision avoidance, but heavy centralized planning methods that handle maze-like environments and dense formations (like @whoenig's work) are overkill. A simpler solution is some kind of reactive onboard collision avoidance, where each quadrotor attempts to move towards its goal position in a straight line, but reacts appropriately if another quadrotor is near.

We considered implementing something like this as library functionality in Crazyswarm, but implementing it in the firmware has significant benefits:

We would like to contribute our implementation back to the main firmware project. I created this issue to get some design feedback before sending any PR.

Algorithms

One suitable algorithm is buffered Voronoi cells (BVC). Another possibility is optimal reciprocal collision avoidance (ORCA). ORCA gets better results when spatial contention is high, but it requires knowing the velocity of neighbors, and imposes stricter constraints on the kind of control laws that can be followed. Buffered Voronoi Cells only constrains that each robot should stay within its cell during the planning horizon.

I already have a prototype of BVC implemented in C and tested: https://github.com/bitcraze/crazyflie-firmware/compare/master...jpreiss:collision_avoidance. Most the core math is useful for generic applications so it has been implemented in https://github.com/jpreiss/cmath3d. I did not use the model-predictive optimal control from the BVC paper. I use a simple projection of the desired direction vector into the Voronoi cell polytope. It runs in less than 1ms on my PC, so I believe it will be fast enough to run at an appropriate rate on the Crazyflie's STM32.

Software Design

High-level commands

Currently there are only two fundamental control modes in the Crazyflie firmware:

Go to a goal subject to collision avoidance is a new mode. It cannot be implemented as modification to the trajectory-following mode, because high contention may require stopping and waiting for a long time, and spatial deviations from the original trajectory could be large. The notion of pre-planning the entire trajectory from start to end is no longer valid.

The new mode could be initialized by a new CRTP high-level command COMMAND_GO_TO_COLLISION_AVOID or similar.

Knowing other Crazyflies' positions

When a motion capture system is in use, each Crazyflie already receives the radio packets containing positions of all other Crazyflies on its radio channel. Currently, we ignore those. In crtp_localization_service.c, for the EXT_POSE_PACKED (link) and EXT_POSITION_PACKED (link) packet parsing code, we do nothing if the packet id field doesn't match our own id. But now, we would need to store them.

crtp_localization_service.c currently "pushes" new measurements of the Crazyflie's own position into the state estimator's queue. I do not think that mechanism is appropriate here, because there may be more than one place in the firmware where other Crazyflies' positions are needed. Instead, I would suggest adding APIs to crtp_localization_service.h to allow any other part of the firmware to "pull" the other positions. For example, something like

bool isOtherIDActive(uint8_t id);
struct vec getOtherIDPosition(uint8_t id);

To cut down on statically allocated memory, we can store the positions as they come on the radio - as 16-bit integer values of millimeters - instead of the 32-bit floating point values of meters that the rest of the firmware would need.

Currently, this approach means we can only scale collision avoidance up to the number of Crazyflies that can share one radio channel. However, it would not be hard to modify the base station software (such as Crazyswarm) to send all position measurements on all channels. Per-Crazyflie commands and other bandwidth consumers such as logging would still be sent/received only on the appropriate radio.

Algorithm code location

I would suggest implementing the core algorithm in a "library" style similar to the current piecewise polynomial library in pptraj.c. This allows compiling the algorithm on a PC for prototyping and testing.

It would probably make sense to put the integration with the overall planning/control framework part in planner.c. However, I am beginning to question the value of the state machine logic in planner.c, since there is also complicated state machine logic in crtp_commander_high_level.c related to emergency timeout stabilization and switching between low- and high-level modes. In the long term, I think it might simplify things to merge them together.

whoenig commented 4 years ago

I have simple artificial potentials implemented in some branch which I can share. It addressed the issues you raised similar to how you suggest it. One problem is still the downwash. We can also likely share the Glas Neural network, which also has a safety module that can be used without NN.

On Sat, Mar 28, 2020, 18:11 James Alan Preiss notifications@github.com wrote:

In multi-quadrotor applications like mapping and search/rescue, where the task requires the robots to spread out, the risk of collisions is inherently low. We want to ensure collision avoidance, but heavy centralized planning methods that handle maze-like environments and dense formations (like @whoenig https://github.com/whoenig's work) are overkill. A simpler solution is some kind of reactive onboard collision avoidance, where each quadrotor attempts to move towards its goal position in a straight line, but reacts appropriately if another quadrotor is near.

We considered implementing something like this as library functionality in Crazyswarm https://github.com/USC-ACTLab/crazyswarm, but implementing it in the firmware has significant benefits:

  • Most importantly: makes the functionality available to a larger set of Crazyflie users.
  • Minimizes communication bandwidth - only need to send one packet with the goal position and any algorithm control parameters.
  • Minimizes computational load on the base station PC.
  • Keeps the Crazyswarm control script simple.

We would like to contribute our implementation back to the main firmware project. I created this issue to get some design feedback before sending any PR. Algorithms

One suitable algorithm is buffered Voronoi cells (BVC) https://www.youtube.com/watch?v=rrDmcE94OQw. Another possibility is optimal reciprocal collision avoidance (ORCA) https://www.youtube.com/watch?v=zmTgj4SKftc. ORCA gets better results when spatial contention is high, but it requires knowing the velocity of neighbors, and imposes stricter constraints on the kind of control laws that can be followed. Buffered Voronoi Cells only constrains that each robot should stay within its cell during the planning horizon.

I already have a prototype of BVC implemented in C and tested. It is not on GitHub yet. I did not use the model-predictive optimal control from the BVC paper. I use a simple projection of the desired direction vector into the Voronoi cell polytope. It runs in less than 1ms on my PC, so I believe it will be fast enough to run at an appropriate rate on the Crazyflie's STM32. Software Design High-level commands

Currently there are only two fundamental control modes in the Crazyflie firmware:

  • Follow a piecewise polynomial trajectory from the high-level commander.
  • Follow streaming low-level setpoints from the PC.

Go to a goal subject to collision avoidance is a new mode. It cannot be implemented as modification to the trajectory-following mode, because high contention may require stopping and waiting for a long time, and spatial deviations from the original trajectory could be large. The notion of pre-planning the entire trajectory from start to end is no longer valid.

The new mode could be initialized by a new CRTP high-level command COMMAND_GO_TO_COLLISION_AVOID or similar. Knowing other Crazyflies' positions

When a motion capture system is in use, each Crazyflie already receives the radio packets containing positions of all other Crazyflies on its radio channel. Currently, we ignore those. In crtp_localization_service.c, for the EXT_POSE_PACKED (link) https://github.com/bitcraze/crazyflie-firmware/blob/9e47ecfe69677b785f54fe6167f5a6ce048642ba/src/modules/src/crtp_localization_service.c#L179-L189 and EXT_POSITION_PACKED (link) https://github.com/bitcraze/crazyflie-firmware/blob/9e47ecfe69677b785f54fe6167f5a6ce048642ba/src/modules/src/crtp_localization_service.c#L199-L207 packet parsing code, we do nothing if the packet id field doesn't match our own id. But now, we would need to store them.

crtp_localization_service.c currently "pushes" new measurements of the Crazyflie's own position into the state estimator's queue. I do not think that mechanism is appropriate here, because there may be more than one place in the firmware where other Crazyflies' positions are needed. Instead, I would suggest adding APIs to crtp_localization_service.h to allow any other part of the firmware to "pull" the other positions. For example, something like

bool isOtherIDActive(uint8_t id); struct vec getOtherIDPosition(uint8_t id);

To cut down on statically allocated memory, we can store the positions as they come on the radio - as 16-bit integer values of millimeters - instead of the 32-bit floating point values of meters that the rest of the firmware would need.

Currently, this approach means we can only scale collision avoidance up to the number of Crazyflies that can share one radio channel. However, it would not be hard to modify the base station software (such as Crazyswarm) to send all position measurements on all channels. Per-Crazyflie commands and other bandwidth consumers such as logging would still be sent/received only on the appropriate radio. Algorithm code location

I would suggest implementing the core algorithm in a "library" style similar to the current piecewise polynomial library in pptraj.c. This allows compiling the algorithm on a PC for prototyping and testing.

It would probably make sense to put the integration with the overall planning/control framework part in planner.c. However, I am beginning to question the value of the state machine logic in planner.c, since there is also complicated state machine logic in crtp_commander_high_level.c related to emergency timeout stabilization and switching between low- and high-level modes. In the long term, I think it might simplify things to merge them together.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/bitcraze/crazyflie-firmware/issues/567, or unsubscribe https://github.com/notifications/unsubscribe-auth/AA3SIPJKTUAMUSDXEZTVL63RJ2N4DANCNFSM4LVZATZQ .

jpreiss commented 4 years ago

I currently account for downwash in BVC using the ellipsoid collision model by finding the separating planes in a stretched coordinate system.

Based on my understanding, BVC might be superior to potential fields because it should not be as sensitive to tuning parameters. (You mentioned this in a previous email on this topic.)

Can you link to the branch with the potential field implementation for reference on software design discussions?

jpreiss commented 4 years ago

Edit: my implementation of the core BVC algorithm is in https://github.com/bitcraze/crazyflie-firmware/compare/master...jpreiss:collision_avoidance

whoenig commented 4 years ago

A public copy of the firmware code for our GLAS paper is available at https://github.com/whoenig/crazyflie-firmware/tree/GLAS-iros2020.

Knowing other Crazyflies position

There are two new functions in the localization service: locSrvGetStateByCfId and locSrvGetStateByIdx. Internally, this keeps an array, so GetStateByCfId will trigger a linear search. This array is filled whenever the radio received new position information for a CF other than itself and has a maximum size hard-coded in the firmware. This approach is very similar to what you proposed. One difference is that we also store the timestamp, this way the client code can filter "old" information (example usage).

Algorithm code location

Similar to what you suggested, we put our algorithm in a separate file that can be easily tested on a PC: platform-agnostic code, FreeRTOS Glue Code. Now the central issue is how to include it in the firmware. Our solution is somewhat hacky, but works well with the high-level mode and can be easily enabled and disabled. In particular, we put our code in the planner. First, planning the current goal is augmented. This augmentation has different modes (we have a simple artificial potential and the neural network) and one can switch between the modes using a parameter. In each mode, we augment the current desired (high-level) setpoint by keeping a hidden state of a double integrator. The hidden state is continuously updated whenever none of the modes is active.

One could also put this logic somewhere else, especially if you want to be able to enable this collision avoidance in low-level flight as well. However, the principle will likely remain the same (this logic would then likely move into the stabilizer in order to work in all possible flight modes).

krichardsson commented 4 years ago

Hi!

I think this is an interesting feature that would benefit the platform and I have some input on the design.

We (Bitcraze) are happy to add any functionality that we believe is beneficial for a larger group of users and that is useful in multiple use cases if possible (not too application specific).

The intention of my comments/suggestions is to tweak and broaden your proposal in this direction if possible. Some might be hard to implement, but I just want to bring them up for consideration.

Knowing other Crazyflies position

It would be nice to be able to use the functionality with other positioning systems (for instance the Lighthouse) and without Crazyswarm, that is without broadcasting position information from a central system. I'm thinking that it might be possible to generalize the functionality of knowing other Crazyflies positions into a module, and that position data for other CFs could be fed (or sourced) from P2P communication. Another possible source of positioning data could be a future UWB mixed mode (TWR + TDoA) scheme where other CFs positions are transmitted in UWB packets.

I could imagine other use cases, other than collision avoidance that could use the positions of other CFs as well. This could probably be a useful module on its own right.

The SITAW (Situation awareness module)

There is already a SITAW module, would it be possible to extend it with this new functionality? From an application point of view it would make sense to get notifications of possible collisions for instance.

Collision avoidance as a utility

From an application point of view it would be nice to have access to all parts of the solution (as modules or utilities), where the application "glues" it all together. That approach makes it possible to tap in and modify at various points in the chain as opposed to a solution that is part of the platform.

Maybe the "glue" part should be an (out of tree build) application, while the bulk of the code is part of the platform?

jpreiss commented 4 years ago

@whoenig your "Knowing other Crazyflies position" implementation is close to what I had in mind. The hard-coded limit on number of neighbors could be a limitation for large swarms, especially if the system is extended to support decentralized localization. Maybe this is an appropriate use case for dynamic memory allocation? Either way, this is a minor issue IMO.

I initially thought it wouldn't be possible to implement collision avoidance as a modification/augmentation layer in between the commander and controller in the stabilizer, but looking at your implementation where you track the position and velocity of the goTo trajectory, I might have been wrong. I think the key is to preserve some "clamping" behavior in the collision avoidance so it doesn't go crazy if the setpoint is very far away from the state.

@krichardsson I agree that accessing the positions of other Crazyflies ought to be available for other localization methods besides mocap. P2P seems really promising because it would allow each CF to perform its own sensor fusion with its IMU and share a higher quality state estimate.

If I understand your proposal correctly: we add something like peer_localization.h that would provide similar APIs to what we have already discussed, but would potentially get information from many other sources besides crtp_localization_service. To avoid blocking the radio task for nontrivial computations, would we then have queues for all the different input sources, similar to the estimatorEnqueue* functions in estimator.h currently?

Re. SITAW, I am not too familiar with this module, but it seems like some high level functionality like bool sitAwCollisionLikely() would be implemented as a consumer of the information exposed in peer_localization.h.

Re. utility/application architecture, I agree the "glue" part is pretty small. Aside from peer localization, the main computational task for BVC is projecting a vector into a convex polytope, which I have already implemented as a standalone function in math3d.h. My question is, how will applications interact with the existing commander/planner structure? I need to read the design doc for applications, I guess.

krichardsson commented 4 years ago

To avoid blocking the radio task for nontrivial computations, would we then have queues for all the different input sources, similar to the estimatorEnqueue* functions in estimator.h currently?

Yes, if you need to perform computations that risk to block the radio task, the best option is probably to create a new task that runs that computation. Queues is a good tool for task-to-task communication.

My question is, how will applications interact with the existing commander/planner structure?

Not sure, my comment was more on a philosophical level :-)

I need to read the design doc for applications, I guess.

It is very sparse, it will not take long https://github.com/bitcraze/crazyflie-firmware/blob/master/docs/userguides/app_layer.md The app layer is still work in progress, let me know if you have any questions.

jpreiss commented 4 years ago

Hi all, I was thinking more about how the collision avoidance module is best integrated into the rest of the planning and control framework. At first I thought that we needed a new planner mode, but after more consideration, I think we can make it work in a more flexible way. However, I have one unresolved design question, denoted in bold near the end.

I suggest the following architecture:

Collision avoidance is implemented as a layer in between the commander and the controller. It consumes a setpoint_t and outputs a new setpoint_t. Attitude control modes are not supported.

Aside from the Voronoi cell itself, relevant parameters are:

Using the shorthand p == setpoint.position and v == setpoint.velocity:

setpoint.mode.x == modeAbs and v == 0:

Interpret the setpoint to mean "go to this position and stop".

Let p' denote the closest point in our cell to p. If p' is within H * Vmax away from current position, use it as our new position setpoint. If not, use the appropriate point on the line segment between p' and current position.

setpoint.mode.x == modeAbs and setpoint.velocity != 0:

Interpret the setpoint to mean "go to this position and hold this velocity after you get there".

If p is outside our cell, the situation has diverged from the original plan, and most likely the nonzero velocity is pointing further away from our cell. Therefore, replace setpoint.velocity with zero and degrade to v == 0 behavior described above.

Otherwise, consider the point p + Hv.

setpoint.mode.x == modeVelocity:

Interpret the setpoint to mean "fly with this velocity".

Scale by the maximum possible 0 <= s <= 1 such that (current position) + Hsv is inside our cell and ||sv||_2 <= Vmax.

whoenig commented 4 years ago

Just a few notes:

Collision avoidance is implemented as a layer in between the commander and the controller. It consumes a setpoint_t and outputs a new setpoint_t.

In my understanding, this is exactly what the situation awareness framework already does. Currently, it has support to detect attitude issues (e.g., drone flipped upside down), but it is generic enough to handle position setpoint adjustments, too.

I prefer thinking about the different modes in terms of typical use-cases; not all combinations make sense. In particular:

  1. Waypoint tracking: your first case; logic makes sense
  2. Velocity tracking (e.g., user controls world-frame velocity with a joystick): your third case. Logic is fine, too.
  3. Trajectory tracking: your second case. Here, my interpretation is different: This case means we try to track a position and velocity (in other words: we want to track position, and the given velocity is our feedforward term). As such, I suggest to treat it as the first case, but in addition scale the velocity term so that it is a consistent feedforward term with respect to the updated/constrained position to help the tracking controller.

As a side note: I don't like the setpoint struct that much for the reason that not all combinations make sense. It would be nicer to have an enum and union for the desired "flight mode". Then each controller could also decide which flight modes it can actually support properly. In the situation awareness framework, we would only enable the collision avoidance in some of the flight modes. The firmware controllers already go that route and only support setpoint mode combinations that are "common", but it is hard to decipher which combinations are even valid.

jpreiss commented 4 years ago

I agreee SitAw could be a home for this functionality. I guess the alternative would be a new module implementing something like collisionAvoidanceUpdateSetpoint that runs right before sitAwUpdateSetpoint. @krichardsson any opinion?

Re. your case 3, can you mathematically define "scale the velocity term so that it is a consistent feedforward term with respect to the updated/constrained position to help the tracking controller"?

I agree with your comments about the setpoint struct, but any changes should be deferred to a standalone project IMO. I think the concept of "flight mode" could also potentially subsume the state machine logic in planner.c, the switching between low-level and high-level modes in commander.c, and the low-level setpoint packet types in crtp_commander_generic.c.

whoenig commented 4 years ago

For case 3, this would need to solve a planning problem instance. IMO the easiest way would be to use the one-piece trajectory planner from planner.c to plan from the current state to stop at a position within your BVC. Then the velocity is the derivative of the polynomial at t=dt. Another option would be to use a double integrator model instead.

Agreed that any setpoint struct changes are not within the scope of this issue. I only pointed it out to argue that we do not need to support all possible setpoint struct configurations in this design, but can focus on commonly used ones.

jpreiss commented 4 years ago

@whoenig OK, I think I misinterpreted your initial comment before. I was breaking down the cases in terms of the commander's intent, but it sounds like you are breaking down the cases in terms of the controller's abilities. So if I understand correctly, when you say:

I suggest to treat it as the first case, but in addition scale the velocity term so that it is a consistent feedforward term with respect to the updated/constrained position to help the tracking controller,

you are disagreeing not with my statement

If p is outside our cell, the situation has diverged from the original plan, and most likely the nonzero velocity is pointing further away from our cell,

but only with my statement

therefore, replace setpoint.velocity with zero and degrade to v == 0 behavior described above,

and the reason you disagree is because setting setpoint.velocity to zero fails to take advantage of the active controller's ability to track a velocity command.

You propose to instead generate an appropriate velocity command by solving a polynomial trajectory planning problem subject to:

and then evaluating the velocity of the polynomial trajectory at dt where dt is the rate at which collision avoidance will run, so probably less than the controller but still many times per second.

If my interpretation is correct, then won't the resulting velocity command be very close to the current velocity in most cases?

whoenig commented 4 years ago

Your interpretation is correct! I think the velocity command will be close to the current velocity if dt << H. Further, H might be something that we'll need to compute/estimate dynamically, based on the dynamics limits (e.g., v_max, a_max). If we only consider those two derivatives, it might be easier to go with a double integrator model, where we estimate H using the maximum possible deceleration. At the end, we try to approximate an optimal control problem with constraints to stay within our BVC, similar to how the original BVC paper had it formulated. We could also try to use cvxgen or similar tools to generate code that can actually solve the optimal control problem online (although I feel this might be "overkill" for the intended use-case.)

jpreiss commented 4 years ago

I was thinking that H would be a user specified parameter they can send over the radio when enabling collision avoidance, or a param.

I agree that CVXGEN is most likely overkill. I also think it is not a good dependency to introduce into the crazyflie-firmware project because it requires an account on the CVXGEN website to generate code, the code generator is not open source, and the generated code is hard to understand.

I lean towards keeping the collision avoidance really simple and allowing it to do things like generate discontinuous velocity commands. We could then put more effort into making the controllers handle step inputs smoothly, which would have a much larger payoff across many Crazyflie use cases.

Personally, I think it's OK to include the caveat that collision avoidance is only designed to handle slow movement, at least in the initial version.

whoenig commented 4 years ago

Sounds good! One comment: OSQP is a good open-source alternative to cvxgen, see https://stanford.edu/~boyd/papers/pdf/osqp_embedded.pdf. (Again, I don't think we should use it for this issue, but just for future reference.)

jpreiss commented 4 years ago

OK, so to summarize: in the case when p is outside our cell and the velocity command is nonzero, we throw away the input velocity setpoint but want to take advantage of the active controller's ability to use velocity inputs. Therefore, we use some heuristic to compute a reasonable velocity command to act as a feedforward input with the goal of reaching p' and stopping.

Now, returning to my previous question about what to do if p is within our cell but p + Hv is not:

In case i., I think it is not the responsibility of the collision avoidance module to track trajectories, so we should assume that the downstream controller will quickly reject the disturbance and reach p. Under this assumption, it makes sense to scale v by the maximal s ∈ [0, 1] until p + Hsv is inside our cell.

In case ii., scaling the velocity in the same way effectively is reducing the gain of the upstream controller, which is probably not a big deal.

So in the end, the same behavior seems to make sense for all 3 cases.

knmcguire commented 3 years ago

btw, since this has been merged already, can we close this?