Ruby's Development Guide
Here you can learn about the design principles, code structure and how to easely navigate the Ruby functionalities.
Development General Guidelines
Ruby Processes
Code Structure
Before you proceed, it's recomended to have already cloned the RubyFPV's source code as it's easier to lookup things referenced in this guide and create a better mental picture.
Development General Guidelines
- Code is C/C++, with no major preference of one over the other. Only in some parts of the code, where it makes sense, C is prefered in order to avoid a lot of vtable lookups at each function call;
If it does not make life harder in any way, in a pice of code or functionality, C code is prefered over C++. There is no point of using objects/polymorphism/fancy stuff if not really needed or if it does not make a piece of code or function easier to understand; - Since messages are exchanged between different components, processes, air units and ground units, all messages have the same structure (header + data) and same header structure, in order to facilitate future components and to avoid bugs, issues or performance issues if messages would have to be constantly translated to different formats;
The format used for messages exchange (IPC or radio) is defined in "radiopachets2.h" header file. - Each function should and will check for errors in the input parameters; Errors should be treated as gracefully as possible; no try/catch and exceptions to be used;
- If a functionality depends on a particular file from the permanent storage, it should try to recover (recreate) the missing file, if it's missing;
- If a parameter is invalid, or an error condition is encountered, unless it's a catastrophic one (ie. missing binaries on storage), the code should try to work arround it, even if it means a functionality will work in a degraded state, or even not at all. Not all functionalities are critical, the system is designed so that many of the functionalities are just there to enhance the experience, they are not mandatory;
- Use Hungarian variables names notation. It's easier for the brain to understand a piece of code you read if you automatically deduce what an object is instead of having to scroll to other place or reference another file;
Ruby Processes
Ruby processes are designed around the concept of a controller and a vehicle (relay nodes are just particular types of controllers or vehicles). As such, there are two types of processes (with different code and different resulting binaries): ones that run on a controller, and ones that run on a vehicle.Process: ruby_start
There is one common (entry) process, ruby_start, which is executed when the hardware is booted. This process does some boot checks of the current hardware and software configuration and figures out, based on hardware, conditions or user preferences, if the current hardware should boot as a controller or as a vehicle (relay is just a special case of those).
In turn, ruby_start will execute a "startup" Ruby process specific to controllers (ruby_controller) or specific to vehicles (ruby_vehicle). After that, the job of ruby_start is complete. The other processes takes it over from here on.
This distinction of controllers and vehicles is also visible in the source code structure, as you will see below.
Process: ruby_rt_station / ruby_rt_vehicle
These are the core router components in Ruby. One is for controller, one is for vehicles. Their main job, as explained in the architecture document is to route messages to/from local components, processes or remote components, processes.
Whether we are talking about a local message, on the same piece of hardware, that's to be forwarded between two local components, whether we are talking about a message that's to be send over air, from a controller to a vehicle, for example, all messages have the same structure and a common header.
If we look at the common header (which you can find in "radiopackets2.h"), we will see these following fields that are used in routing logic and to identify source and destination:
typedef struct
{
u32 uCRC; // computed for the entire packet or header only. start point is after this crc.
// Highest byte is set to 0x00 to be able to detect and distinguish radio packets from other systems. (starting in version 8.0)
// After the packet it received and checked for CRC, this field will be updated to contain the radio datarate it was received at (as int)
u8 packet_flags;
u8 packet_type; // 1...150: components packets types, 150-200: local control controller packets, 200-250: local control vehicle packets
u32 stream_packet_idx; // high 4 bits: stream id (0..15), lower 28 bits: stream packet index
// monotonically increassing, to detect missing/lost packets on each stream
u16 packet_flags_extended; // Added in 7.4: it replaced (length of all headers)
// byte 0:
// not used
// byte 1:
// bit 0: 1: send on high capacity links only;
// bit 1: 1: sent on low capacity links only;
// bit 2: 1: requires ACK for this packet
u16 total_length; // Total length, including all the header data, including CRC
u16 radio_link_packet_index; // Introduced in 7.7: monotonically increasing for each radio packet sent on a radio link
// used to detect missing packets on receive side on a radio link, not for duplicate detection
u32 vehicle_id_src;
u32 vehicle_id_dest;
} __attribute__((packed)) t_packet_header;
Some fields are filled in automatically by Ruby router, some are to be used to specify source and destination:
vehicle_id_src / vehicle_id_dest: Each hardware device in Ruby gets assigned, automatically, a unique ID. So, if you have one controller and 3 vehicles, you will have 4 unique IDs. Id 0 (zero) is used for broadcast purposes.
packet_flags: Contains, among other things, a sub-component ID, so that you can route packets locally (or remotelly) to a particular component in the Ruby system (ie. telemetry component, video component, etc);
To read more how routing of packets is done, read this document
Process: ruby_*
All other Ruby processes fulfill a single functionality needed for a FPV system (or custom systems): telemetry, video, RC, custom data streams, etc.
The design goal of each of such process is one of the two below (or both):
- Ingest data from a peripheral (ie. a UART port), packetize it into Ruby packets and send them to Router to be delivered to destination.
- Receive messages from Router and build an output data stream to be consumed by a peripheral (ie. a UART port).
Code Structure
The structure of the Ruby's source code is as follows:
- base
This folder contains code that allows interfacing with the hardware. This way, if a new hardware type must be supported, or a new SOC, or a new operating system, most changes/additions are done here, so that the rest of the code remains as is.
This folder also contains the API for local IPC between processes. This way, if a new IPC mechanism is to be used, it must be updated here only. - common
This contains only generic code, not hardware specific, that's used by multiple processes or components. - r_utils
This generates binaries that are self contained, more like utilities, and that are used for: processing a video recording, updating the configuration of the system after an update, logger service. - r_central
This generates the user interface process. On controllers, this proces is started at boot time, by ruby_controller, which in turn is started by ruby_start.
All interactions of the end user with the user interface, are done through this process.
Also, all OSD info or any additional information shown on the screen (except for the actual video feed) are displayed by this process; - r_i2c
This generates a process that's responsible for communicating with I2C devices. - r_start
This generates the binary that's the root entry point in Ruby, at runtime, when the hardware boots up. - radio
This folder contains all the low level APIs and code specific for interacting with IEEE radios. - renderer
This folder contains the C++ objects used to interact with the hardware rendering on the device. As the rendering to the screen is very OS and hardware specific, this is a factory and specific renderers are created for Raspberry, Radxa and so on.
These renderer objects are used only by ruby_central process. - r_vehicle
This generates the binaries that are specific for vehicles, including the ruby_rt_vehicle router process; - r_station
This generates the binaries that are specific for controllers, including the ruby_rt_station router process; - r_utils
This generates binaries that are self contained, more like utilities, and that are used for: processing a video recording, updating the configuration of the system after an update, logger service. - r_utils
This generates binaries that are self contained, more like utilities, and that are used for: processing a video recording, updating the configuration of the system after an update, logger service.
Related read:
Ruby's Software ArchitectureRadio Links and Interfaces: How They Work
Radio Streams: How They Work
Ruby