3 Oct 2013

APM - simple long range wireless telemetry










This is a short howto on building a simple wireless groundstation for 10-20 km UAV telemetry. For simplicity reasons the groundstation has no tracking capabilities. The user’s input device can be a laptop or a tablet/smartphone (android for now).


The input device connects via bluetooth (SPP) to the groundstation and the signal is relayed from there via RFD 900 telemetry modem (3DR modem does work as well). The RFD 900 modem has a built in diversity antenna system. It can use 2 different antennas. In my case an omni-directional rubber duck antenna for short range and a direction patch antenna for long range. The patch antenna must be pointed roughly into the direction of the UAV (45° angle).


I have used a teensy 3.0 board to do the message buffering/passing between the bluethooth modem, a USB port and the RFD900. The teensy costs roughly usd 20, has 3 hardware UARTs and packs a fair amount of power (Cortex M4 up to 96MHz).


Internals
tripod

Bill of Materials





Configure Bluetooth Modem

The BlueSmirf Gold uses the RN-41 AT command set. By default, the modem should be set to 57.6 kilo baud. This is the default speed for wireless telemetry also, this means it should work aut of the box if wired correctly to the teensy (or directly to the RFD 900 modem). More information on wiring a BlueSmirf to an FTDI cable can be found here.


Wiring

This illustration describes the wiring between teensy 3.0, BlueSmirf Gold and RFD-900.


Blue: Teensy TX <-> Modem RX
Yellow: Teensy RX <-> Modem RX


Teensy 3.0 Pins

Code

The programm that is running on the teensy can do 2 things:
  • just play relay between serial links (passthrough). This is low latency but does not allow modifying packages.
  • buffered. In this mode the serial link will buffer until it receives a full mavlink message. Before sending it to the target, it can be modified or dropped (or routed/duplicated to another serial port).
The code relies on the AP_HAL and a modified GCS_Mavlink library (without FastSerial, we have hardware UARTs and mavlink 0.9 removed).

ArduMavProxy.ino

#include <GCS_MAVlink.h> #include "ArduMavProxy.h"

// #define DBG


// message structs


static mavlink_message_t msg1;

static mavlink_message_t msg2;
static mavlink_message_t msg3;
static mavlink_status_t status1;
static mavlink_status_t status2;
static mavlink_status_t status3;

// Serial devices
static comm_t s_src     = {"", 0, &Serial1, msg1, status1, 0, 1};
static comm_t s_modem   = {"", 0, &Serial2, msg2, status2, 0, 2};
static comm_t s_ext = {"", 0, &Serial,  msg3, status3, 0, 3};

void setup() {
Serial.begin(TELEMETRY_SPEED);
Serial1.begin(TELEMETRY_SPEED);
Serial2.begin(TELEMETRY_SPEED); // FIXME: s_modem.serial->begin() doesn't work
//Serial3.begin(TELEMETRY_SPEED);

// set pins to default state
pinMode(PIN_ARM, OUTPUT);
pinMode(PIN_AUTO, OUTPUT);
digitalWrite(PIN_ARM, LOW);
digitalWrite(PIN_AUTO, LOW);
}
void loop() {

// No passthrough to modem so we queue src packages
uint8_t ret1 = read_packet(&s_src, &s_modem, false);

// TODO: check for comm_t.has_packet
if (ret1) { // we got a complete message from the source
route_packet(&s_src, &s_modem);
flush_packet(&s_src);

#ifdef DBG
Serial.print("Sats: ");
Serial.print(gps_satellites_visible);
Serial.print(", fix: ");
Serial.print(gps_fix_type);
Serial.print("\t");
Serial.print(motor_armed, HEX);
Serial.print("\t");
Serial.println(base_mode, BIN);
#endif

digitalWrite(PIN_ARM, (motor_armed) ? HIGH : LOW);
digitalWrite(PIN_AUTO, (mode_auto) ? HIGH : LOW);
}

// read mavlink package from modem
uint8_t ret2 = read_packet(&s_modem, &s_src, true);
if (ret2) { // we got a complete message from the source
// TODO: implement fast passthrough for 2 channels
route_packet(&s_modem, &s_ext);
flush_packet(&s_modem);
}

#ifndef DBG
// No passthrough to modem so we queue src packages
uint8_t ret3 = read_packet(&s_ext, &s_modem, false);
if (ret2) { // we got a complete message from the source
route_packet(&s_ext, &s_modem);
flush_packet(&s_ext);
}
#endif
}


ArduMavProxy.h
// Get the common arduino functions
#if defined(ARDUINO) && ARDUINO >= 100
#include
"Arduino.h"
#else
#include
"wiring.h"
#endif

#define TELEMETRY_SPEED  57600  
// How fast our MAVLink telemetry is coming to Serial
#define PIN_ARM 13
#define PIN_AUTO 14
#define MAVLINK_FRAME_LENGTH 263

#include <GCS_MAVlink.h>
#include
"include/mavlink/v1.0/mavlink_types.h"
static uint8_t      base_mode=0;

static bool         motor_armed = 0;
static uint8_t      mode_auto = 0;

typedef struct comm_t {
char buffer[MAVLINK_FRAME_LENGTH + 1];
int buffer_count;
Stream *serial;
mavlink_message_t msg;
mavlink_status_t
status;
bool has_message;
uint8_t chan;
};


util.ino
// utility methods

// bit juggling
boolean getBit(byte Reg, byte whichBit) {
boolean State;
State = Reg & (1 << whichBit);
return State;
}


byte setBit(byte &Reg, byte whichBit, boolean stat) {
if (stat)
Reg = Reg | (1 << whichBit);
else
Reg = Reg & ~(1 << whichBit);
return Reg;
}


/**
* flush input buffer
*
* the user must make sure that the buffered packet was used before flusing.
*/
void flush_packet(comm_t *src) {
src->buffer_count = 0;
src->buffer[0] =
'\0';
}


/**
* write buffer to serial
*
* writes a buffer and decoded incoming mavlink serial packet to another
* serial port.
*/
void route_packet(comm_t *src, comm_t *target) {
for (int i=0; i <= src->buffer_count; i++)
target->serial->
write(src->buffer[i]);
//flush_packet(src);
}


/**
* read a mavlink packet
*
* returns 1 if we got a complete packet. returns 0 if we need to read
* more into the stream.
*
* passthrough is for minimal latency. Best used for sniffing or routing only.
*/
uint8_t read_packet(comm_t *src, comm_t *target, bool passthrough) {

//grabing data
while(src->serial->available() > 0) {

// the packet should have been used, flush it to prevent buffer overflows
if (src->has_message) {
src->has_message =
false;
flush_packet(src);
}

char c = src->serial->read();

// buffer the received character
src->buffer[src->buffer_count] = c;
(src->buffer_count)++;

// buffer overflow protection
if (src->buffer_count == MAVLINK_FRAME_LENGTH) {
// flush stream buffer if full
//src->buffer_count = 0;
//src->buffer[0] = '\0';
flush_packet(src);
}

if (passthrough)
target->serial->
write(c);

// try to grab message, decode if complete
if(mavlink_parse_char(MAVLINK_COMM_0, c, &(src->msg), &(src->status))) {
src->has_message =
true;
return 1;
}
}

return 0;
}

Download

The latest source code is available browseable on Github or as zip download.

Connecting

Windows Mission Planner

Windows Mission Planner can be downloaded at ardupilot.com.


You have to options: connect with USB-A to Micro-USB B cable to teensy or use bluetooth. The following section describes the Bluetooth connectivity setup.


Enable Bluetooth and pair with the ground station.
Add bluetooth adapter from task tray
Your device should show up here


Make sure you get the manual pairing option.
Enter PIN

Device Paired
Verify from devices. Right click the device and select Properties » Services (“SPP” should be displayed).


If done, the comm port shown in the last screenshot can be used from MP to connect (57.6K).


DroidPlanner (Android)

Enable Bluetooth and pair with the ground station.


Open DroidPlanner, select settings and configure the modem to BLUETOOTH.

Andropilot (Android)

Enable Bluetooth and pair with the ground station.


After launching the program, click on the [Bluetooth] overlay button to connect.

23 Aug 2013

GoPro lens protection



I just added a GoPro lens protection on the new vanguard. I removed the lense protection from the original GoPro 3 case and hot glued it onto the wooden GoPro mount. This saves me quite some weight.

12 Aug 2013

Multiplex Smart SX 6 Position switch



The Multiplex Smart SX is theoretically a 5 Channel transmitter. The 5th channel is controlled via button/throttle-trim. This channel is not easy to modify. Because I will mostly use flying wing designs (no rudder) I decided to remove the rudder potentiometer and connect the 6-pos switch to that instead.

This leaves me with a 3 channel transmitter (aileron, elevator and throttle) and a 6 position switch. Thanks once more to Sämi for his nice soldering job.

Removing the potentiometer.

Adding the 6 position switch







5 Jul 2013

XFLR5 - airfoil analysis at low Reynolds Numbers


Click on the image for a larger resolution.

I am currently working on a new flying wing design. I was extremely amazed that there is open source software for doing software based wind tunnel simulation. The software package is called XFLR5 and is available as source package and binary package for windows (the debian repo includes a binary as well: apt-get install xflr5).

The animation above shows a flying wing design I am working on. The wing uses an MH-64 airfoil. In the animation you can see the induced drag, the lift and wake turbulence created by an Angle of Attack between -3 up to 12°.

11 Jun 2013

SD Card Extension

I was searching for a way to leave the camera in the plane. Now that it is powered from the main battery, turning on/off is controlled via Autopilot I just needed an easier to access SD card. This is the solution I cam up with so far:




4 May 2013

ResearchDrones - WWT Caerlaverlock


This is a short teaser of the ResearchDrones Scotland trip. Serge Wich and I have been flying the drones for WWT Caerlaverock Wetland Centre. 


Special thanks to Larry Griffin for inviting and supporting us.

We also had a little time to fool around with our drone and a quad. Enjoy.

Cambodia Training - ResearchDrones / ConservationDrones

ResearchDrones and ConservationDrones in Cambodia for training BirdLife, ELIE, CI and WCS staff on the MAJA C.


22 Apr 2013

V-Tail mixer with low pass filter

/**
PPM V-Tail/elevon mixer

Input pins:
 2: aileron/rudder
 3: elevator

Output pins:
 9: servo left
10: servo right
*/


#include <math.h>
#include <Servo.h>

//#define DEBUG
#define START_OUTPUT_PIN 9
#define LOW_PASS_FILTER 50 // difference must be greater than X at 


int Chan1Interrupt = 0; // pin 2, aileron
int Chan2Interrupt = 1; // pin 3, elevator

unsigned long Chan1_startPulse, Chan2_startPulse;
volatile double Chan1_val, Chan2_val = 1500;
volatile double Chan1_val_last, Chan2_val_last = 1500;
long StartMillis=0;
long FrameCounter=0;

Servo ServoArray[2];

double reverse(double val) {
 return (val-1500) * -1 + 1500;
}

double add(double val1, double val2) {
 double out = (val1 -1500 + val2 -1500) + 1500;
 out = (out < 1000) ? 1000 : out;
 out = (out > 2000) ? 2000 : out;
 return out;
}

void serviceServos(long pFrameCounter) {
 
 // do the v-tail mixing 
 double left  = add(Chan2_val, Chan1_val);
 double right = add(reverse(Chan2_val), Chan1_val);
 
 ServoArray[0].writeMicroseconds(left);
 ServoArray[1].writeMicroseconds(right);
}

void setup() {
 attachInterrupt(Chan1Interrupt, Chan1_begin, RISING);
 attachInterrupt(Chan2Interrupt, Chan2_begin, RISING);
 
 ServoArray[0].attach(START_OUTPUT_PIN+0, 900, 2100); // aileron
 ServoArray[1].attach(START_OUTPUT_PIN+1, 900, 2100); // elevator

 StartMillis = millis();
#ifdef DEBUG
 Serial.begin(115200);
#endif
}

void loop() {
 long LocalMillis;
 long LocalFrameCounter;
 LocalMillis = millis();
 LocalFrameCounter = (LocalMillis - StartMillis) / 20;

 if (LocalFrameCounter > FrameCounter) {
  FrameCounter = LocalFrameCounter;
  serviceServos(FrameCounter);
 }
}

void Chan1_begin() {
 Chan1_startPulse = micros();
 detachInterrupt(Chan1Interrupt);
 attachInterrupt(Chan1Interrupt, Chan1_end, FALLING);
}

void Chan1_end() {
 Chan1_val = micros() - Chan1_startPulse;
 detachInterrupt(Chan1Interrupt);
 attachInterrupt(Chan1Interrupt, Chan1_begin, RISING);
 if (Chan1_val < 1000 || Chan1_val > 2000) {
  Chan1_val = Chan1_val_last;
 } else {
  if ((Chan1_val + LOW_PASS_FILTER) > Chan1_val_last || 
      (Chan1_val - LOW_PASS_FILTER) < Chan1_val_last)
   Chan1_val_last = Chan1_val;
  else
   Chan1_val = Chan1_val_last;
 }
}

void Chan2_begin() {
 Chan2_startPulse = micros();
 detachInterrupt(Chan2Interrupt);
 attachInterrupt(Chan2Interrupt, Chan2_end, FALLING);
}

void Chan2_end() {
 Chan2_val = micros() - Chan2_startPulse;
 detachInterrupt(Chan2Interrupt);
 attachInterrupt(Chan2Interrupt, Chan2_begin, RISING);
 if (Chan2_val < 1000 || Chan2_val > 2000) { 
  Chan2_val = Chan2_val_last;
 } else {
  if ((Chan2_val + LOW_PASS_FILTER) > Chan2_val_last || 
      (Chan2_val - LOW_PASS_FILTER) < Chan2_val_last)
   Chan2_val_last = Chan2_val;
  else
   Chan2_val = Chan2_val_last;
 }
}

21 Apr 2013

Elevon / V-Tail mixer with Arduino Pro Mini 5V



I got this new and and really handy r/c transmitter from multiplex: the SMART SX.



It's really a minimal approach. I have been a fan of multiplex for a long time because they have always been pioneers in their domain. This one is the first quality r/c transmitter with a gamepad layout (there are many technical features other will copy sooner or later but I bought it because of the layout primarily).

However, this being an entry level transmitter it is really limited in functionality. It has all the features a modern transmitter needs to have, configurable through 1 (yes ONE) button!

However, Elevon/Y-tail mixing isn't a feature baked in. I needed an elevon mixer for my new flying wing and couldn't get one quickly. Fortunately I have some unused micro controller boards laying around which just waited for this hack.


A 15$ Arduino Pro mini 5V seemed to be suitable (although overkill, I just needed something in a short time). Using 2 input interrupts  1 timer for the Servo.H library and 2 PWM pins as output, there is still a lot of unused hardware.

Code

/**
PPM V-Tail/elevon mixer

Input pins:
 2: aileron/rudder
 3: elevator

Output pins:
 9: servo left
10: servo right
*/


#include <math.h>
#include <Servo.h>

//#define DEBUG
#define START_OUTPUT_PIN 9

int Chan1Interrupt = 0; // pin 2, aileron
int Chan2Interrupt = 1; // pin 3, elevator

unsigned long Chan1_startPulse, Chan2_startPulse;
volatile double Chan1_val, Chan2_val = 1500;
volatile double Chan1_val_last, Chan2_val_last = 1500;
long StartMillis=0;
long FrameCounter=0;

Servo ServoArray[2];

double reverse(double val) {
 return (val-1500) * -1 + 1500;
}

double add(double val1, double val2) {
 double out = (val1 -1500 + val2 -1500) + 1500;
 out = (out < 1000) ? 1000 : out;
 out = (out > 2000) ? 2000 : out;
 return out;
}

void serviceServos(long pFrameCounter) {
 
 // do the v-tail mixing 
 double left  = add(Chan2_val, Chan1_val);
 double right = add(reverse(Chan2_val), Chan1_val);
 
 ServoArray[0].writeMicroseconds(left);
 ServoArray[1].writeMicroseconds(right);
}

void setup() {
 attachInterrupt(Chan1Interrupt, Chan1_begin, RISING);
 attachInterrupt(Chan2Interrupt, Chan2_begin, RISING);
 
 ServoArray[0].attach(START_OUTPUT_PIN+0, 900, 2100); // aileron
 ServoArray[1].attach(START_OUTPUT_PIN+1, 900, 2100); // elevator

 StartMillis = millis();
#ifdef DEBUG
 Serial.begin(115200);
#endif
}

void loop() {
 long LocalMillis;
 long LocalFrameCounter;
 LocalMillis = millis();
 LocalFrameCounter = (LocalMillis - StartMillis) / 20;

 if (LocalFrameCounter > FrameCounter) {
  FrameCounter = LocalFrameCounter;
  serviceServos(FrameCounter);
 }
}

void Chan1_begin() {
 Chan1_startPulse = micros();
 detachInterrupt(Chan1Interrupt);
 attachInterrupt(Chan1Interrupt, Chan1_end, FALLING);
}

void Chan1_end() {
 Chan1_val = micros() - Chan1_startPulse;
 detachInterrupt(Chan1Interrupt);
 attachInterrupt(Chan1Interrupt, Chan1_begin, RISING);
 if (Chan1_val < 1000 || Chan1_val > 2000) {
  Chan1_val = Chan1_val_last;
 } else {
  Chan1_val_last = Chan1_val;
 }
}

void Chan2_begin() {
 Chan2_startPulse = micros();
 detachInterrupt(Chan2Interrupt);
 attachInterrupt(Chan2Interrupt, Chan2_end, FALLING);
}

void Chan2_end() {
 Chan2_val = micros() - Chan2_startPulse;
 detachInterrupt(Chan2Interrupt);
 attachInterrupt(Chan2Interrupt, Chan2_begin, RISING);
 if (Chan2_val < 1000 || Chan2_val > 2000) { 
  Chan2_val = Chan2_val_last;
 } else {
  Chan2_val_last = Chan2_val;
 }
}

22 Mar 2013

C shared objects exported to python (ctypes)

Some C Projects need a quick extension which is not performance relevant. Sometimes I wish to use python because it is available on all modern *nix systems. Also, programming in python is very efficient.

To solve this I have created a small example project which puts the functionality into a shared object (dll in windows-speak) which can be used from python.

The library:

lib.c
This library contains a simple function which appends the string "->aaa" to an existing string. This is an interesting example since it involves dynamic memory allocation and pointers.


#include <stdlib.h>
#include <string.h>
#include "lib.h"

//int main() {return 0;}

char *append(char* str) {
 //char *n = "";
 char *b = "->aaa";
 char *n = malloc(strlen(str) + strlen(b) + 1);
 n[0] = '\0';
 strcat(n, str);
 strcat(n, b);
 return n;
}


lib.h
char *append(char* str);


test.c
used for testing the the library from C.

#include <stdio.h>
#include "lib.h"

int main(int argc, char* argv[]) {
 printf("%s\n", append(argv[1]));
 return 0;
}

Compiling
#!/usr/bin/env bash
gcc -std=c99 -Wall -c -fPIC lib.c -o lib.o
gcc -std=c99 -Wall -shared -Wl,-soname,libt.so -o libt.so lib.o
gcc -std=c99 -Wall -o test test.c -L. -lt
rm lib.o


pylibt.py
Because python uses dynamic data types we need to give some hints about data types. Also pointers need to be declared.
#!/usr/bin/env python

from ctypes import *

cdll.LoadLibrary("./libt.so")
_libt = CDLL("libt.so")

#def initpylibt():
# pass

def append(s):
 _append = _libt.append
 _append.argtypes = [c_char_p]
 _append.restype = c_char_p
 return _append(s)


test.py
let's use the python module from a script.

#!/usr/bin/env python

import pylibt

ret = pylibt.append("0123456789as dfasdf asdf asdf asdf asf asdf")
print ret