Motivation
If there is one thing I don’t like about PhotonFTC its how convoluted and messy the source code is. If statements nested inside If statements, seemingly contradictory reflections, and the worst sin of all, zero comments. I’ve been meaning to come back to this but honestly, it wouldn’t help much anyway.
Lets start with the basics: What exactly are we hoping to achieve with Photon? Its pretty simple, we want to parallelize commands to help remove bottlenecks. Now that is a lot of words, so lets break down that that means.
If you have ever tried to time optimize FTC code, you might have noticed a lot of delays coming from “hardware calls”, methods such as motor.setPower() or servo.setPosition(). On average, each of these calls takes around 2 ms (or 0.002 seconds). While that seems fast, people often are setting 5 or 6 motors in addition to 3 or 4 servos per loop. This can very quickly add up. While loop times of 30 ms are usually fine, Photon was made to address a specific need in swerves, which require very fast loop times in order to control the modules correctly.
Digging Slightly Further
As a quick tangent, I want to quickly examine why it takes 2ms to execute a command. Lets follow a command as it gets generated!
The User Code
At the user code, we have the following line.
mymotor.setPower(1.0); This calls the setPower method of the DcMotor object, which looks something like this:
synchronized public void setPower(double power) {
// Power must be positive when in RUN_TO_POSITION mode : in that mode, the
// *direction* of rotation is controlled instead by the relative positioning
// of the current and target positions.
if (getMode() == RunMode.RUN_TO_POSITION) {
power = Math.abs(power);
} else {
power = adjustPower(power);
}
internalSetPower(power);
}
protected void internalSetPower(double power) {
controller.setMotorPower(portNumber, power);
} This calls an internal method in the attached LynxDcMotorController class. This class is rather big with a lot of methods, but we are going to focus on the run mode RUN_WITHOUT_ENCODER.
This line in perticular is the one we are interested in
command = new LynxSetMotorConstantPowerCommand(this.getModule(), motorZ, iPower); As you know (if you don’t already), a control hub is composed of two components. The android board, which the user code runs on, and the expansion hub, which has direct control over the various I/O like motor controllers and servo ports. The way these two boards communicate is via packets, predefined message formats. The LynxSetMotorConstantPowerCommand, for example, sends a motor power to the expansion hub to set.
The Packet Exchange
Communication can essentially be split up into 3 phases: Send, Process, Respond.
SDK Default: Each command waits for the previous one to fully complete (send → process → respond) before the next starts. This is safe but slow.
Time for 6 packets: 9.0ms
Send
In this phase, the android board sends the packet value over UART (a communication protocol) to the expansion hub. The expansion hub operates at a baud rate of 460800 baud, which means this phase takes approximately 0.3-0.5 ms to transmit the packet.
Process
During this phase the expansion hub processes the incoming packet and does whatever action is needed to execute the packet. In our motor example code, this consists of reading the power to be set and communicating that power to the motor controllers. This also tends to take around 0.5-1ms, but it highly depends on the action. For example, servo writes actually tend to process slightly faster then motor writes, but its generally a negligable difference.
Respond
In this phase the expansion hub actually responds back to the android board. For actions such as a motor or servo write, this is generally an “ACK”, or acknowledgement, packet. If something went wrong, the expansion hub sends a “NACK”, or No Acknowledgement packet. For reads, this packet will also contain the data requested. Since this is also at the same baud rate, it also takes around 0.3-0.5ms to complete.
High Level HAL
Now that we know how communication works on the FTC control system, lets take a look at the brief rules in place for communication. The most important one is what you might see referred to as the “synchronization lock”. Essentially, in the FTC SDK, only one command may be send at a time. Whenever a command is sent, the communication system will prevent any new command from being sent until the expansion hub returns an ACK or a NACK. My question was simple: Is this necessary?
## What Photon Does The answer is pretty straightforward: no. The lock to only send one command at a time, while recommended by both REV and FIRST, is not strictly needed. The expansion hub has some "buffer space" that it can use to store multiple commands and process them in order, sending back ACKs and NACKs as it processes the commands. There is a down side however. That buffer space is limited, and if you send too many commands at once, bad things can, and usually will, happen. As a result, you need to be very careful that buffer space doesn't fill up.What PhotonFTC does is very simple. When a command is queued to be sent, it calls a method in the LynxModule object to queue the command to be sent. PhotonFTC inserts itself at runtime, replacing the LynxModule object in the devices in the hardware map, and hijacks that command. Instead of passing it along freely, it instead transmits the command itself, bypassing the single command lock in the communications system.
.. note:: Interestingly enough, this is completely possible to do on your own! All of the methods to do exactly this are public methods, and you could write your own device drivers to accomplish the same task. This is how FTC 7244 ran a precurser to Photon in Freight Frenzy
The bulk of Photon are what I like to call the “transmission rules”. Essentially, through a combination of careful testing and analysis, I managed to sketch out rules to prevent the expansion hub from crashing during use. It involved some trial and error (shoutout to FTC 1002 and FTC 16379, both of whom had… unfortunate incidents testing an alpha version of Photon), but eventually I carved out the basic specifications.
The most important rule, by far, is the 15 packet rule. This rule means that at any given point, the exapansion hub cannot be sent more then 15 packets, or it becomes at risk for crashing. If that is obeyed, then everything else generally runs very smoothly.
## Side note: Why no RS485? I've been asked a couple times why RS485 does not work with PhotonFTC. The answer is pretty simple. RS485, as a protocol, has no mechanism for "collision avoidance". What this means is if both expansion hubs connected with RS485 try to "talk" to each other at the same time, the transmissions will be lost. As a result, you can't sent multiple commands at once due to the risk of the second hub trying to respond at the same time the first hub sends a new message. USB, on the other hand, has collision avoidance (in a sense) so you can avoid this by using USB. Who knew?