Skip to content

Server Setup

CurryKitten edited this page Jan 31, 2020 · 4 revisions

Right, there's a fair bit to go through here, and as my "lets just type everything out as I think about it" attempt of documenting it, bear with me, as it'll probably need a few rewrites.

Installing the Arduino code
So obviously you need an Arduino board for this... if you haven't got one you are scupered, so go out and get one and then check back in later. I wrote the code based on the Arduino Leonardo board http://arduino.cc/en/Main/ArduinoBoardLeonardo this was based on me asking a friend (who knew about these things) what I should buy. However, I don't believe I'm doing anything that shouldn't work on another board - certainly the very popular Arduino Uno shouldn't have an issue.

When you connect up your Arduino board and run the IDE, a new serial port should automagically appear. In Mac/Linux it'll be something along the lines of /dev/tty.usbmodemfd141, on a Windows machine something like COM8: Take a note of this as you'll need it for the Python server code.

The code was written around the Turnigy 9X (which is a badged version of the FlySky 9X and is sold under a variety of 9X names). If you have the 9X, then there are no changes to make. Grab the code from the git repository, upload it to your Arduino board, and relax.

To run on a different TX module, it's necessary to know something about how the PPM stream is made up. Namely the framelength, pulse gap, and whether the pulses are high or low. If you don't know, try it with the default parameters first and see what results you get. Depending on the feedback, I created a few utilities to read PPM from a radio in order to work these out - but would need some work in order to make it useful to other people.

If you do need to make changes and you know what they are, then the first bit of code to attack is here

#define channumber 8         // How many channels we'll work on, though some parts are...
int channel[channumber+1];   // This array hold the PPM usec values 0 is the sync pulse
int ch[5];                   // This array is used to read the percentage values of ea...
int PPMout = 9;              // The pin on the Arduino we output PPM on
int pulsegap = 300;          // The gap between the channels (300 is for a Turnigy 9X)
int frameLength = 20;        // The duration in millisecs of a frame
int startpack=0;             // We use this to check for a valid start of a serial read
int endpack=0;               // ..and this for the end of the serial read
int lastFrame=0;
int curMillis=0;
int i;

As the comments suggest, the PPMOut value denote the pin on your Arduino board you are sending the PPM signal on. I can't remember why i picked 9, so feel free to change as this suits you.

More relavent to people on different radio systems is the pulsegap. The pulsegap represents the length of time between each channel pulse from the transmitter in microseconds.

Although the frameLength is also listed as a variable that suggests how long in milliseconds the frame length of the entire PPM packet would be, it's actually left over code (oops) from a non-interrupt driven version which was less accurate. So if your frame length is different then you'll need to change the value of how often the timer interrupt is called. Which happens in the following section of code -

// UseTimer1 to setup an ISR every 20ms
  cli();          // disable global interrupts
  TCCR1A = 0;     // set entire TCCR1A register to 0
  TCCR1B = 0;     // same for TCCR1B

  // set compare match register to desired timer count:
  OCR1A = 312;  // At 16Mhz, 20ms = 320,000 clock cycles, so 312 * 1024 = 319488
  // turn on CTC mode:
  TCCR1B |= (1 << WGM12);
  // Set CS10 and CS12 bits for 1024 prescaler:
  TCCR1B |= (1 << CS10);
  TCCR1B |= (1 << CS12);
  // enable timer compare interrupt:
  TIMSK1 |= (1 << OCIE1A);
  // enable global interrupts:
  sei();

The basic maths behind this is as follows: timer1 is a 16Mhz time, and so that amounts to 16,000,000 cycles per second

In order to call the interrupt every 20 milliseconds, we work out how many clock cycles to wait using the sum: (16,000,000 / 1000)* 20 = 320,000.

The registers are only 16 bits, so we can't wait for a number as large as 320,000 so we use a pre-scaler. I don't want to go into how these work, but at the end of it you just need to take your clock cycles, in this case 320,000 and divide it by 1024, which gives us 312 (and a bit) Use this as the basic calculation if you need to change the your frame length.

The last bit of the code you might need to change is if your pulses are actually low or high. The 9X radio puts out a high signal to indicate a pulse, and a low signal to indicate a frame gap. Some radios do the reverse.

To add: Oscilliscope output of a PPM stream

ISR(TIMER1_COMPA_vect)
{
// The ISR generate the PPM. This was written with a Turnigy 9X in mind, however
// it's simple to change to a radio which uses LOW pulses to generate it's PPM
// just by reversing the LOW/HIGH in the loop

for ( i = 1; i < 5; i = i + 1 ) {
digitalWrite(PPMout, LOW); // Start on the LOW pulse
delayMicroseconds(pulsegap); // wait for the pulse gap
digitalWrite(PPMout, HIGH); // .. and do our HIGH pulse
delayMicroseconds(channel[i]); // Finish off pulse
}
// Finish frame with sync pulse
digitalWrite(PPMout, LOW);
delayMicroseconds(pulsegap);
// The sync is HIGH, so leave the pin in that state under the next interrupt
digitalWrite(PPMout, HIGH);
}

If you need to reverse the LOW/HIGH pulses, just literally change the LOWs to HIGHs in the above section of code, and the HIGHs to LOWs.

Connecting up your hardware
Currently, we've only tested this using the Arduino board connected to a separate RC transmitter module. However, there's no reason making up a cable to your trainer port won't work - and will of course give you the ability to take over your model, should your guest go AWOL with it.

Between the Arduino board and the transmitter module we need 3 connections. One from one of the digital pins to PPM on the TX (we use 9 by default) A connection from GND between Arduino and TX, and 5V from Arduino to battery on the TX.

A quick word about power. I suggest you power your Arduino independently of your computer. Although it will take the 5v from the USB connection and attempt to pass this though, there's no guarantee that this voltage will stay consistent. One thing I picked up from testing, is that lack of consistent power to the TX will give you jittery servos.

All else that's needed from the Arduino is the USB connection between itself and your computer... but you probably already had that plugged in.

Configuring the server code
Did you write down the serial port that your Arduino decided it was on, because this is the point where you need it. Take a look at this segment from RC_Remote_server_UDP,py

# The serial interface is going to change on a system to system basis, and may change on the same machine
print "Available Serial Ports:"
print serial.tools.list_ports.comports()
ser = serial.Serial('/dev/tty.usbmodemfd141', 9600) 

So amend the variable ser to the correct serial port. The line above it we can treat as debug if you like. The original idea was to let the user select a serial port form the list displayed, but this list_ports method turned out to be not cross-platform supported. So for now, hardcode it.

The only other code change that might be needed is here -

//Lets get a socket to listen on
myHost = '192.168.1.100'
myPort = 6666

You'll want to change this to the IP address of your computer, and if you like change the port number as well. Of course, if you have an address on your computer that's 192.168.x.x then odds on this is just an assigned address from your router, so unless your client is also connected to your router, he won't be able to connect to you.

What you'll need to do is login to your router and define a port forwarding rule, so that UDP traffic on port 6666 is forwarded to 192.168.1.100 (in the above case)

To add: Router port forwarding screen shot

Starting the server
Start up the server on the command line with -

$ python RC_Remote_serverUDP.py

It should fire up and give you a screen like this

... and instantly I notice a typo in the second "Dual" sigh. Really the first thing you want is for your partner client to connect to you and start wiggling the sticks around - it'll give you a good idea if everything is working. At the point that looks ok, you can take a look at the few options you have.

This was built around controlling a car with the left stick operating the camera and the right stick operating throttle and steering. If you get your partner to move both his sticks up/down left/right you need to make sure that the model reacts as he'd expect it to. Often we found things were reversed, and this is why by pressing numbers 1-4 you can reverse any of the channels in order to make things react correctly.

The throttle dual rate was built in as a safety feature - as in testing this I gave someone control of a car that would do about 20mph, and was being driven inside my house... bad idea. So the dual rate is there to mean that full stick on the client means actually 50% (by default) of the throttle is applied. Raise/lower this to your own tastes.

Finally the Steering Dual rate - what is this. It's something we noticed when using a playstation type of joystick. The sticks are built into a circular base, so if you try to push all the way forward and turn fully so can't. You can't hit that top left/top right section because it's curved off. So what we can do with the steering dual rate is make the steering go full lock with less joystick movement - fixing the problem of the rounded off corners on a Playstation joystick.

Clone this wiki locally