ENG-CANSAT-MISSION1-CAPTURE
Introduction
Before starting this point, we recommand to follow all the sensors testing steps (BMP280 sensor, TMP36 Sensor, RFM69HCW radio, RFM69HCW Testing and onboard NeoPixel). It contains all the details about the wiring, install needed libraries and conduct basic testing. |
The following Wiring is used to capture
- Air temperature
- Air pressure
and transmitting the information via the RFM69HCW radio module.
Wiring
Wire the barometric sensor
The BMP280 is wired on the I2C bus of the Feather.
Wire the temperature sensor
Then connect the TMP36 sensor as follows:
- The pin 1 (on the left) to a power source (3.3V),
- The pin 3 (the the right droite) to the ground/GND.
- The pin 2 (middle one) to the A3 analogue input.
Wire the radio module
Finally wire the RFM69HCW radio as follows:
Feather M0 Express | RFM69 |
3V | VIN |
GND | GND |
MO | MOSI |
MI | MISO |
SCK | SCK |
6 | CS |
9 | G0 |
10 | RST |
Download the code
The code is available for download on the GitHub associated to this wiki.
About testing
Now, we will move forward in several steps.
- Getting data from sensors + send them it over the serial connexion (to confirm good working) + transmit over radio
- Testing the radio reception
- Going autonomous (removing Serial Connexion waiting) + add the Lipo
The code proposed here under has been tested up to 23197 iterations without issue, time when we decided to ends the test :-) .
Once uploaded to your Feather, open the Serial Monitor and set it to 9600 bauds. The sketch would wait until you open the Serial Monitor to start transmitting the data.
You should see the following messages appears on the Serial Monitor.
Where we could see the transmitted messages with the packetnum packet index, timing and data.
The screen also displays the ACK acknowledgement send back by the receiver.
Structuring the data
The radio module only sends buffer of binary data to the receiver. This is a bit rough but efficient.
So to transport the data to the receiver, we need to transform the values (float, integer) into their string representation.
When having multiple data in their string representation is not enough, they must also been organized.
The final format must be easy to parse and very compact (smaller is the radio message and higher is the chance for him to get to the ground without error).
We propose the following format:
:data1|data2|data3|data4;/r/n
where:
- : is the begin of data stream
- ; is the end of data stream
- /r/n are optional carriage return + line feed characters.
This will would make the messages user friendly when the the messages are viewed in a console or terminal. - | is the separator between data items.
- datax are the string representation of the various data. The characters ;:| are forbidden in this area.
we would also recommend to use:
- packetnum as data1. packetnum is a simple variable increment of one unit after each transmission. This would allow the receiver to detect lost message (since it would exist holes in the numbering of received messages).
- timing_info as data2. This would help to create timing chart or time base data analysis. We suggest to use the Arduino's millis() function which count the number of milliseconds since the last microcontroler reset.
As explained later in the code the packet_str variable contains the message to be transmitted to the ground. The Arduino's String class would ease the transformation of data to their string representation.
String packet_str = String( ":"+String(packetnum,DEC)+"|" );
packet_str.concat( String( ms,DEC)+"|" );
packet_str.concat( String( temperature, 2 )+"|" );
packet_str.concat( String( bme_hpa, 2 )+"|" );
packet_str.concat( String( bme_temp, 2 )+";\r\n" );
LEDs and Error management
Being able to understand rapidly what's happening inside your object is essential to rapidly fix the issue.
The best is to figure out what's happening is to use LED, blink status, heartbeat.
By doing so, no need to open a Serial Monitor or diagnostic tool to figure out the status of the object.
The NeoPixel LED does turn GREEN when the Feather M0 switch on to normal operation (when it runs your Arduino Sketch).
In the following sample, we do take the control over the NeoPixel LED to switch it off at the end of setup() function. This means that all buses and devices are properly initialized.
The RADIO_LED wired on the Pin 13 is used to signal radio status when emitting a message.
LED operation | Description | Fix the issue |
NeoPixel GREEN | The setup() function did not complete initialization because of a crash. | Check the wiring of sensors. Test each sensor separately (with their tests code). If this not working, remove all sensors except the one you are testing. |
NeoPixel OFF | The setup() did complete successfully. The main loop() is not running. | Nothing to do here, just check the RADIO_LED for more informations. |
RADIO LED = 1 pulse 50ms | The LED is pulsed for each successfully send message + getting ACK from the receiver. The code wait 500ms max for the ACK. | Nothing to do here. |
RADIO LED = 2 pulse 50ms + pause 100ms | Message send but error while decoding the ACK response. | This is not critical, the most important is that the message was sent successfully. |
RADIO LED = 3 pulse 50ms + pause 150ms | Not ACK message received within the 500ms after message was sent. This can be interpreted as "Is there someone listening the message?" because there are not reply. |
This is not critical, the most important is that the message was sent successfully. |
The code explained
Here some explanation about the mission1-serial-radio-capture.ino sketch used in the CanSat.
This Arduino sketch would:
- Wait for the serial connexion to be established before starting the sketch
- Collect the sensor data
- Send it to serial connexion
- Send it over the radio connexion
First, the script will includes all the needed libraries.
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_NeoPixel.h>
#include <RH_RF69.h>
Then, it defines the parameters for the radio module and the pinout used to wire the RFM69HCW radio module to the Feather M0.
The last line create the object rf69 to control the module.
#define RF69_FREQ 433.0
#define RFM69_CS 6
#define RFM69_INT 9
#define RFM69_RST 10
#define RADIO_LED 13
RH_RF69 rf69(RFM69_CS, RFM69_INT);
Defining the parameters to control the NeoPixel LED available on the board. That LED is wired on the Pin 8.
The last line creates an objet pixel which is a Pixels Strand of only 1 pixel length.
#define NEOPIXEL 8
#define NUMPIXELS 1
Adafruit_NeoPixel pixel = Adafruit_NeoPixel(NUMPIXELS, NEOPIXEL, NEO_GRB + NEO_KHZ800);
Defining the parameter and objects for temperature and pressure sensor.
#define temperaturePin A3
Adafruit_BMP280 bme; // wired with I2C
Initialize the serial connexion @ 9600 bauds, the BMP sensor, the radio module (init_radio_module()) and pixel.
The NeoPixel is turned when the setup() function is complete.
void setup() {
Serial.begin(9600);
// wait until serial console is open
while (!Serial) { delay(1); }
if (!bme.begin()) {
Serial.println("Could not find a valid BMP280 sensor, check wiring!");
while (1);
}
init_radio_module();
// everything is right! So switch off neopixel
pixel.begin();
pixel.setPixelColor(0, pixel.Color(0,0,0)); // switch off
pixel.show();
}
Now, we can focus on the main loop.
The first step is to send the column header (so we know what are the data) if not done yet.
Then we reads the sensors (as we have tested them, this should not be a surprise). We also capture the time with the function millis(), so the ms variable contains the number of milliseconds since the last reset.
Finally, we do increment the packetnum variable. This would allows to track lost packets on the receiver side.
packet_str is the message to send via radio. It is composed with String objects and concatenation operations. String are welcome to transform float into string representation since most common float to string C standard functions would fail to work properly onto Arduino alike plateforms.
The key function to transform the String object' into a C buffer is packet_str.c_str() which offer an access to the underlying array of bytes (exactly what the radio module library would need).
The remaining of the radio transmission code is almost the same as the RFM69HCW module testing code (except that error messages are remplaced by Blinking LED).
bool header_send = false;
// packet number increment at each data transmission
int16_t packetnum = 0;
void loop() {
// --- SEND COLUMNS HEADER -------------------
if( !(header_send) ){
send_header();
header_send = true;
}
// --- READ SENSORS ---------------------------
float voltage = getVoltage(temperaturePin);
float temperature = (voltage - .5) *100;
float bme_temp = bme.readTemperature();
float bme_hpa = bme.readPressure();
unsigned long ms = millis();
packetnum += 1; // increment
// --- Compose the Message to send ------------
String packet_str = String( ":"+String(packetnum,DEC)+"|" );
packet_str.concat( String(ms,DEC)+"|" );
packet_str.concat( String( temperature, 2 )+"|" );
packet_str.concat( String( bme_hpa, 2 )+"|" );
packet_str.concat( String( bme_temp, 2 )+";\r\n" );
// send to Serial
Serial.print( packet_str.c_str() );
// Send over Radio
rf69.send((uint8_t *)(packet_str.c_str()), packet_str.length());
rf69.waitPacketSent();
// Now wait for a reply
uint8_t buf[4]; // We limit the quantity received data
uint8_t len = sizeof(buf);
if (rf69.waitAvailableTimeout(500)) {
// Should be a reply message for us now
if (rf69.recv(buf, &len)) {
Serial.print(": ");
Serial.println((char*)buf);
Blink(RADIO_LED, 50, 1); //blink LED once, 50ms between blinks
} else {
Serial.println("Receive failed");
Blink(RADIO_LED, 50, 1); //blink LED once, 50ms between blinks
}
} else {
Serial.println("No reply, is another RFM69 listening?");
Blink(RADIO_LED, 50, 3 ); // blink 3 times, 50ms between blinks
}
// Going to next round
}
The init_radio_module() function is called from the setup().
This function does all the stuff to initialize the RFM69HCW modules. Set the transmission power, the frequency and the encryption key.
void init_radio_module() {
pinMode(RADIO_LED, OUTPUT);
pinMode(RFM69_RST, OUTPUT);
digitalWrite(RFM69_RST, LOW);
Serial.println("Feather RFM69 TX Test!");
Serial.println();
// manual reset
digitalWrite(RFM69_RST, HIGH);
delay(10);
digitalWrite(RFM69_RST, LOW);
delay(10);
if (!rf69.init()) {
Serial.println("RFM69 radio init failed");
while (1);
}
Serial.println("RFM69 radio init OK!");
// Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM (for low power module)
// No encryption
if (!rf69.setFrequency(RF69_FREQ)) {
Serial.println("setFrequency failed");
}
// If you are using a high power RF69 eg RFM69HW, you *must* set a Tx power with the
// ishighpowermodule flag set like this:
rf69.setTxPower(20, true); // range from 14-20 for power, 2nd arg must be true for 69HCW
// The encryption key has to be the same as the one in the server
uint8_t key[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
rf69.setEncryptionKey(key);
pinMode(RADIO_LED, OUTPUT);
Serial.print("RFM69 radio @");
Serial.print((int)RF69_FREQ);
Serial.println(" MHz");
}
This function send the header information to the Serial monitor.
Ideally, this function should also send it via the radio.
void send_header() {
String s1 = String( F("***HEADER***\r\n") );
Serial.print( s1 );
String s2 = String( F(":counter|time_ms|temperature|pressure_hpa|temp2;\r\n") );
Serial.print(s2);
String s3 = String( F("***DATA***\r\n") );
Serial.print( s3 );
}
Helper function used to blink a LED. Note that a pause of 3 time the blinking time. This will ease the identification of blink code into other blinking patterns.
void Blink(byte PIN, byte DELAY_MS, byte loops) {
for (byte i=0; i<loops; i++) {
digitalWrite(PIN,HIGH);
delay(DELAY_MS);
digitalWrite(PIN,LOW);
delay(DELAY_MS);
}
// exit: wait 3 times the delay
delay( 3* DELAY_MS );
}
This function returns the voltage for the analog Pin.
It converts a digital value between 0 & 1024 (from ADC) to voltage between 0 & 3.3 volts.
float getVoltage(int pin){
// each unit equal 3.3 / 1024 = 3.2 millivolts
return (analogRead(pin) * .0032);
}
Fault tolerant design
The goal is to transmit the data to the ground station.
The code of the Emitter (this section) and Receiver (next section) are doing the job.
However, what would happens to your data if the antenna did break? All the data are lots!
This is where the "Extra Flash" would be a great help!
As showed earlier, it is also possible to store/save the data into the Flash.
A good approach would be:
- to save the data in the Flash
- then send it over Radio.
In this way, the data stays available inside the CanSat and could be extracted as suited.
Written by Meurisse D. from MC Hobby - License: CC-SA-BY.