Full Metal Kilobot
Summary
Now that we know the basics, let's tackle the real interesting thing about kilobots : make them communicate.
This tutorial is inspired by the military training scenes in Kubrick's "Full Metal Jacket" movie. We will use two kilobots : one will be the instructor, the other will be the rookie.
Therefore, we are going to write two distinct programs :
- the instructor's program, generating and sending random training instructions
- the rookie's program, executing instructions as fast as possible
What you will learn
- How to create a message with the
message_tstructure - How to generate random numbers with the
rand_hard()function - How to send messages with
kilo_message_tx - How to receive and read messages with
kilo_message_rx
Create messages
Messages for kilobots are contained in the message_t structure, which has 3 fields :
type, from 0 to 127, describing the type of the message. For this tutorial, we will only use the macroNORMALdata, an array of length 9, storing the real content of the messagecrc, an hash code, computed with themessage_crc()function, used to check that data was not corrupted during transmission.
We can define a message like this :
message_t message; // This message will contain the answer to the ultimate question of life, universe and everything.
message.type = NORMAL;
message.data[0] = 42;
message.crc = message_crc(&message);
Create random numbers
Kilobots have two functions to generate random numbers :
rand_soft()generates a random number from a seed. You can provide a specific seed using therand_seed()function.rand_hard()generates a random number from the kilobot's hardware, namely its battery precise voltage.
As rand_hard() is slower than rand_soft(), it is common to use rand_soft() when we need random numbers multiple times per second. In that situation, we can use rand_hard() inside rand_seed() to initialize the RNG.
In this tutorial, we will only use the rand_hard() function as we do not need efficiency.
Instructor's program
Let's get into the real code !
Instructions
The first step is to define a protocol used by the two kilobots to communicate. It seems difficult but it really is fairly simple.
We have 3 different possible instructions :
- "Show me your war face!" ⟶ Light the led
- "Turn on yourself x times!" ⟶ Set motors to do x turns on itself
- "Run x cm and come back!" ⟶ Set motors to run x cm, turn around and come back
A simple protocol would be : "Each instruction has an ID, from 1 to 3, stored in the first byte of data. Instructions 2 and 3 have a parameter, stored in the second byte of data."
So for example, "Turn on yourself 3 times!" would translate to the following message :
message_t message;
message.type = NORMAL;
message.data[0] = 2; // Turn on yourself...
message.data[1] = 3; // ...3 times!
message.crc = message_crc(&message);
We can now write a simple program that will generate a random instruction :
message_t instruction;
int instructionID;
int instructionParam;
void makeInstruction() {
// Creates an instruction to be given to the rookie
instruction.type = NORMAL;
instruction.data[0] = instructionID;
instruction.data[1] = instructionParam;
instruction.crc = message_crc(&instruction);
}
void generateInstruction() {
// Gives an instruction to the rookie
// Choses the instruction
instructionID = (rand_hard() % 3) + 1; // Instructions have a specific number, from 1 to 3
// Depending on the instruction, generates a random parameter
switch (instructionID) {
case 1:
// Rookie must show his war face ! (Turn on his led)
instructionParam = 0;
break;
case 2:
// Rookie must run x centimeters, then come back.
instructionParam = 3 + (rand_hard() % 8); // Cm to be runned, from 3 to 10
break;
case 3:
// Rookie must turn around on himself x times.
instructionParam = 1 + (rand_hard() % 3); // number of turns, from 1 to 3
break;
}
makeInstruction();
}
Note
Note how we put instruction, instructionID and instructionParam as global variables. It makes it possible to access them from anywhere in the code, though we must be careful with the way each part of the program manipulates them.
Message handling
To handle communication, we need to define two functions. One (commonly named message_tx()) will be called when sending a message, the other (commonly named message_rx()) will be called when receiving a message.
We also have to define a flag, which is a variable with value 0 or 1 describing the "state" of the robot. In our case, the instructionGiven flag tells wether or not an instruction has been given to the rookie. If its value is 0, the instructor should generate and send a new instruction. If it's 1, the instructor waits for the rookie to finish.
int instructionGiven = 0;
message_t* message_tx() {
// This function returns the instruction to send to the rookie
return &instruction;
}
void message_rx(message_t *msg, distance_measurement_t *dist) {
// This function is called when the rookie has finished executing his order and is ready for a new one.
instructionGiven = 0;
}
Note
Notice the two parameters of the message_rx function. They are mandatory to define the function, but of no use for the instructor.
Setup and loop
The setup function for the instructor is rather minimalistic, it's even empty in fact. The instructor does not care about anything except yelling orders repeatedly.
In the loop function, we will either generate a new instruction or wait for the rookie. When generating a new instruction, the instructor will indicate the instruction chosen by turning on its LED with a specific color. When waiting, the instructor blinks yellow.
void setup() {
}
void loop() {
if (!instructionGiven) {
// Gives an order. Blinks a specific color depending on the given order
generateInstruction();
switch (instructionID) {
case 1:
set_color(RGB(1,0, 0)); // Blinks red
break;
case 2:
set_color(RGB(0, 1, 0)); // Blinks green
break;
case 3:
set_color(RGB(0, 0, 1)); // Blinks green to
break;
}
delay(500);
set_color(RGB(0,0,0));
instructionGiven = 1; // Changes the flag
}
else {
// Waits for the rookie. Blinks yellow
delay(500);
set_color(RGB(1,1,0));
delay(500);
set_color(RGB(0,0,0));
}
}
Note
Even if the setup() function is empty, we must define it, as it must be passed to the kilo_start() function.
Put it together
The only thing left to do is to define the main function. kilo_message_tx and kilo_message_rx are system variables used to specify which function to call when sending/receiving a message.
int main() {
// initialize hardware
kilo_init();
// Registers the function to call when sending a instruction
kilo_message_tx = message_tx;
// Registers the function to call when rookie says he is ready
kilo_message_rx = message_rx;
// start program
kilo_start(setup, loop);
return 0;
}
Rookie's program
Most of the aspects of the rookie's program have already been seen in the instructor's program. In this section, we will only focus on the new things.
Message handling
Sending messages
The rookie must only send a message to say that he is ready, so the message doesn't need to contain any data : the mere fact that he is sending a message already tells the purpose of the message. And as you can see above, the instructor never cares about the content of the message.
The rookie will also use a flag to tell wether he is ready for an instruction or not.
These facts give the following code :
#include <stddef.h> // Defines 'NULL' macro
message_t ready;
int waitingForInstruction = 0;
void setup() {
// Waits to be called by the instructor
waitingForInstruction = 1;
// Initialize the message
ready.type = NORMAL;
ready.crc = message_crc(&ready);
}
message_t* message_tx() {
// Sends a message if the rookie is ready. If not, sends nothing.
if (waitingForInstruction) {
return &ready;
}
return NULL;
}
Receiving messages
As for the instructor's program, the rookie will receive message with the message_rx() function. Here, the msg parameter is useful to retrieve the content of the message :
int instructionID = 0;
int instructionParam = 0;
void message_rx(message_t *msg, distance_measurement_t *dist) {
// This function is called when the rookie receives an instruction
instructionID = msg->data[0];
instructionParam = msg->data[1];
waitingForInstruction = 0;
}
The loop
In the loop function, the rookie will either execute an instruction, or wait to receive one. When waiting, it blinks yellow.
int straightSpeedCalibrator = 1000; // Delay in milliseconds to go straight on 1cm
int turningSpeedCalibrator = 5000; // Delay in milliseconds to make 1 turn around
void loop() {
if (waitingForInstruction) {
// Waits for orders. Blinks yellow
delay(500);
set_color(RGB(1,1,0));
delay(500);
set_color(RGB(0,0,0));
}
else {
switch(instructionID) {
case 1:
// Rookie shows his war face
set_color(RGB(0, 1, 1));
delay(500);
set_color(RGB(0,0,0));
break;
case 2:
// Rookie runs x centimeters, then comes back
for (int i = 0 ; i < 2 ; i++) {
spinup_motors();
set_motors(kilo_straight_left, kilo_straight_right);
delay(instructionParam * straightSpeedCalibrator);
set_motors(kilo_turn_left, 0);
delay(turningSpeedCalibrator);
set_motors(0, 0);
}
break;
case 3:
// Rookie turns around on himself x times
for (int i = 0 ; i < instructionParam ; i++) {
spinup_motors();
set_motors(kilo_turn_left, 0);
delay(turningSpeedCalibrator);
set_motors(0, 0);
}
break;
}
waitingForInstruction = 1;
delay(2000); // Waits two seconds before executing the next instruction
}
}
Note
Notice the use of straightSpeedCalibrator and turningSpeedCalibrator. These variables are used to indicate the time the kilobot takes to do a specific movement. Set them to the most fitting values depending on your kilobot specificities.
Put it together
The main() function is nothing new at this point:
int main() {
// Initializes hardware
kilo_init();
// Registers the function to call when receiving a instruction
kilo_message_rx = message_rx;
// Registers the function to call when sending a message
kilo_message_tx = message_tx;
// Starts program
kilo_start(setup, loop);
return 0;
}
We are done !
You can now admire two kilobots in a tough military training. You've mastered the art of communication and are ready to get into the next tutorial!