Engineering Concepts

This section describes the internals of Cake Robotics Library. You don’t need this knowledge if you just want to use Cake—the same way you don’t need to know how a piston engine works in order to ride a car.

Mission Statement

Cake Robotics, as a set of projects and services, aims to make it easy and efficient to program robots in the lower end of production scale spectrum.

The observation that clarifies this mission statement is that in small-scale robotics projects, development costs are the economic bottleneck—in contrast to manufacturing costs in large-scale projects.

CRL is the core framework that our users interact with in their codes. Therefore, the role of CRL is to provide the developer experience required to fulfill the mission of Cake Robotics project as a whole.

Design Philosophy

Ease of Use

It is decided that in CRL we put ease-of-use over almost everything else. This means:

  • Only minimal software engineering knowledge should be required to use the library.

  • Only minimal configuration should be required to use the library.

Naturaly, this philosophy tends to produce opinioated software. To counter this, we try to keep the opinioated design in the outer layers of the library. Therefore, CRL would still be usable by the power users—especially from the open-source community—who prefer to use software in more sophisticated configurations.

Architecture

In the basic form, every Cake project consists of a main.py file and a props.yaml (or .json) file. CRL comes with a command cake run <path_to_project_directory> that can be used to run the robot program on the robot.

In other words, Cake project is typically runs in a managed fashion: User code goes to a file like main.py but the file has no main() function. It contains init() and loop() functions (much like Arduino), and the manager script, i.e. cake run reads this file and executes it.

This allows the host script to scan the props file and make the required configurations before running the user code. On the flip side, this restricts user code to a predefined structure. That’s why there’s an alternative, meant for advanced users, in which the the main function is written by user but they should call robot.init_from_props(props) function before running any other cake code. This method is what cake run does internally. Therefore, no matter if the user uses cake run or manually calls init in a self-contained script, behavior of cake modules is the same.

This is a good example of being easy to use for general users, without limiting advanced users. It is notable that for sake simplicity and consistency, the latter way (i.e. calling init manually) is not emphasized in the user documentation and tutorials.

With either usage, the user imports cake functionalities using from cake import Robot or robot command in their script. Then, commands like robot.wheels_1.set_velocity(2) will work without any extra configurations. This is because, upon initialization, cake reads the props object and makes the required configurations.

For example, if in the initialization step Cake detects a wheels_1 key with type: wheels in the props, robot.wheels will be created by instantiating an internal Wheels object. The object itself may run a managed ROS node, used for tasks like broadcast user commands, e.g. robot.wheels.set_velociry, to a ROS topic. The object may also run other ROS nodes, for example, drivers that convert these messages to hardware signals. This is why every object has a .get_dependencies() member function, used in automatic bundling process, which returns a list of ROS packages, etc. that should be available before the object can work under the specified configuration.

This design allows for hierarchy too. For example, a Wheels object may implement a factory pattern in which different drived classes will be instantiated based upon the props file. Composition is possible too. For example, dependencies of a class can be determined by merging the dependencies of the objects within that class.

It’s also notable that robot.wheels will fail if wheels_1 is not defined in the props file. Also, if more that one hardware with wheels type are defined, the call will cause all the wheels to perform the action, unless specified otherwise using an optional argument. So generally, robot object serves as a set of instantiated objects (or lazily instantiated objects). The end user does not have to deal with object oriented paradigms. Also, some of the functionalities are provided as lazy objects.

Codebase Structure

TODO