Thursday, 13 February 2020

Approximating a trailer's path

Several years ago, Brian Tischler developed his own solution for using GPS to record farm machinery coverage in a field and to automatically turn things on and off as the machine crossed into areas that it had already been in. This is known in the industry as "section control." Brian's program, AgOpenGPS, and the community that surrounds it, is incredibly useful, education, and affordable. Already quite a number of farmers throughout the world have build their own GPS solutions for their farm machines including using GPS to steer with, providing straight rows and saving money reducing overlap of costly applications of fertilizer, seed, or other chemicals.

Initially, AgOpenGPS acted as if the implement a farmers was pulling was fixed to the back of the tractor, which is often the case when using 3-point hitch equipment. However if a trailing implement was being pulled, AgOpenGPS would think it was straight back from the tractor, when it fact it was trailing and swinging shorter during turns. My first, and so far only, contribution to AgOpenGPS was an algorithm for approximating where the trailer is tracking

The algorithm

The algorithm I came up with is not based on any research or literature, and it's only an approximation of the real path. It does not take into account side drafting or slipping of the trailer. There are three things that factor into the computation. First, the new GPS position of the tractor. Second, the length of the tongue of the trailer, from where it attaches to the tractor to the center of yaw of the trailer, which is usually the axle. Third, the old computed position of the center of yaw point on the trailer, from the last iteration. In general terms, the algorithm is composed of three steps, repeated each time a new GPS position is known.

  1. Calculate the angle or bearing from the new GPS position to the position where the trailer was before.
  2. Along that bearing, find the location that is the distance equal to the length of the tongue away from the new GPS position.
  3. Assign the trailer position to be that position calculated in step 2.

The more often you measure position and perform these calculations, the calculated trailer position will more closely approximate the real trailer's path.

Whether we are dealing with longitude and latitude, and using Haversines methods and ellipsoidal or spherical means of calculating distances, bearings, and new positions after a given bearing and distance, or if we are dealing with rectangular grid coordinates and simple trigonometry, the effect is the same.

Rectangular coordinates

Here's a diagram showing the original state of a trailer and the result of the first iteration overlaid to illustrate how this works using rectangular coordinates, where e is for "easting" (x coordinate) and "n" is for "northing" (y coordinate):

In the diagram, the first blue cross represents the axle and tongue of the trailer. The green line represents where the GPS location reported for the tractor moved from V0 to V1. The new trailer position can be calculated using trigonometry for rectangular coordinates. But since we're really dealing with similar triangles here, it can also be calculated by using nothing other than the Pythagorean Theorem and applying a ratio, as can be seen in the diagram.

Using latitude and longitude (spherical coordinates)

Using latitude, longitude, the haversine formula, and another position algorithm, it looks like this in C++:

void move_distance_bearing( double &lat, double &lon,
                          double heading, double distance)
{
 double offset = distance / EARTH_RADIUS; 
 double latr = RADIANS(lat);
 double lonr = RADIANS(lon);

 double lat1sin = sin(latr); 

 double lat1cos = cos(latr);
 double distcos = cos(offset);
 double distsin = sin(offset);

 heading = RADIANS(heading);

 lat = asin( lat1sin * distcos +
             lat1cos * distsin * cos(heading) );
 lon = lonr + atan2( sin(heading) * distsin * lat1cos,
                    distcos - lat1sin * sin(lat) );

 lat = DEGREES(lat);
 lon = DEGREES(lon);
}

void trailer_move(double lat, double lon, 
                  double &trailer_lat, double &trailer_lon, 
    double heading, double hitch_length)
{
 //step 1. Calculate bearing from new GPS position to
 //        last trailer position
 if (trailer_lat >= -90) {
  //if last trailer position was known
  heading = haversine_bearing_degrees(
   lon, lat, trailer_lon, trailer_lat );
 } else {
  //otherwise just translate backwards along
  //the current GPS heading (assume trailer is
  //straight behind).
  heading = heading + 180;
 }

 //step 2. Find location that along that bearing that is 
 //        hitch_length from the current GPS position.
 move_distance_bearing (lat, lon, heading, hitch_length);

 //step 3. That location becomes the new trailer position
 trailer_lat = lat;
 trailer_lon = lon;
}

The function, haversine_bearing_degrees() is not given here, but it's an implementation of the standard haversine formula. Also not known are macros DEGREES(), RADIANS(), and the EARTH_RADIUS defines, which anyone can implement. The radius should be in whatever units you use for the other distance and hitch_length variables.

The Haversine and the new position formulas above are based on a spherical approximation of the Earth, which has inaccuracies. An ellipsoidal method would be more accurate. However, compared to working in UTM coordinates which use an ellipsoidal model, at my elevation the differences are often within a cm or two, which is enough for our purposes, especially given that this is just an approximation anyway.

Only an approximation

As said before, this algorithm is only an approximation. As you can see from the diagram above, the calculated path the trailer makes will not be a smooth, continuous arc. Rather it will be a bunch of little straight lines that track to the inside of the real path somewhat. The more often you iterate the calculation, provided you had very fine GPS position updates, the closer it would be come to the actual arc. In real farming experience, it's close enough to work for coverage mapping and section control, even with spraying.

Links

DIY RTK Part 2 - ZED-F9P Base Station

The ZED-F9P is a low-cost GPS receiver capable of everything you need to get sub-inch GPS RTK fixes, useful for everything from surveying to steering farm machines. Regardless of GPS receiver brand, RTK requires at least two GPS receivers. One is a base station that continuously makes observations of various signal characteristics, which are transmitted to the other GPS receiver, often called the rover. Since the base station is, by definition, not moving, the rover can use these observations to correct for various sources of error and refine its own position down to within about an inch. It's very remarkable and the ZED-F9P can resolve to an RTK fix in just a couple of seconds.

RTCMv3

As mentioned in the previous part, there is a standardized format for sharing these observations with roving GPS receivers, RTCMv3. RTCMv3 defines various message types, associated with the different GNSS constellations including GPS, Glonass, Galileo, Beidou, and others. Using all these constellations together makes the system more robust. Modern RTCMv3 message types encapsulate a wide variety of observations and information into the packets. The ZED-F9P supports the most recent types, called "Multiple Signal Messages," type 7. Fortunately the Trimble 372 also accepts these MSM7 messages.

Message Types Required

The message types that we use with the ZED-F9P are

  • 1077 - GPS
  • 1087 - Glonass (only actually useful for ZED-F9P rovers as far as I can tell
  • 1097 - Galileo
  • 1127 - Beidou

Those messages should be transmitted at a rate of at least 1/second.

Other message types we'll use include:

  • 1005 - Stationary RTK Reference Station ARP
  • 1008 - Base station antenna descriptor (not provided by ZED-F9P)
  • 1230 - Glonass base station bias characteristics (probably unused by F9P

Those messages can be transmitted every 10 seconds or more.

The ZED-F9P can generate all of these messages and transmit it out any of its onboard serial ports, which can be connected to radio modem, or using software on a single-board computer or microcontroller with wifi, to an NTRIP Caster.

Injecting type 1008 for Trimble 372

The Trimble 372 will establish an RTK fix if it can receive the following RTCMv3 messages:

  • 1005 (or 1006)
  • 1008
  • 1077
  • 1087
  • 1230

The 1008 message is not generated by the F9P, however the 372 doesn't actually need any of the information contained in that message, so even a blank message will do. I wrote a little program for an Arduino Uno that reads RTCMv3 messages in one serial port, adds the 1008 message, and transmits the combined results out the other serial port. Thus if a radio or NTRIP server takes the output from the Arduino, the 372 will get an RTK fix.

The code is posted at GitHub.

#include 

/*
  Reads in a stream of RTCM3 data on the software serial
  port, outputs it to the built-in serial port.  If
  a message type of 1005 or 1006 is detected, a blank
  1008 message will be inserted into the output stream,
  which should allow a ZED-F9P RTK base station to work with
  Trimble roving receivers, which require 1008.

  Pin 8 is RX from the radio module (unused)
  Pin 9 is TX to the radio module

  Pin 0 is RX from the F9P
  Pin 1 is TX to the F9P (unused)

  For Uno or similar board. Other boards might require
  different pins for the AltSoftSerial port.  See the
  AltSoftSerial documentation for more information.
 */

// Blank RTCM3 type 1008 message
const char packet1008[12] = { 0xd3,0x00,0x06,0x3f,0x00,0x00,0x00,0x00,0x00,0x99,0x25,0xca };

AltSoftSerial mySerial(10,9); // RX, TX

void setup() {
 Serial.begin(57600);
 while (!Serial) {
 ; // wait for serial port to connect. Needed for native USB port only
 }

 // set the data rate for the SoftwareSerial port
 mySerial.begin(57600);
}

void loop() {
 byte c;
 int length;
 unsigned int type;

 int count;
 c = 0;

 //Look for start of RTCM3 message
 while (c != 0xd3) {
  if (Serial.available()) {
   c = Serial.read();
   mySerial.write(c); //pass it through to the radio
  }
 }
  
 //Okay we might have found one, lets get the length
 count = 0;
 length = 0;
 while (count < 2) {
  if (Serial.available()) {
   c = Serial.read();
   mySerial.write(c);
   length = (length << 8) + c;
   count ++;
  }
 }
 length = length & 0x07ff; //isolate only the least significant 10 bits
 
 //identify the message type
 count = 0;
 type = 0;
 while (count < 2) {
  if (Serial.available()) {
   c = Serial.read();
   mySerial.write(c);
   type = (type << 8) + c;
   count ++;
  }
 }
 type = type >> 4; //isolate type form the most significant 12 bits

 //Now pass through the rest of the message
 count = 0;
 while (count < length + 1) {
  //read in the message body, less the 2 type bytes, and then
  //the 3 CRC bytes, so length + 1.
  if (Serial.available()) {
   c = Serial.read();
   mySerial.write(c);
   count ++;
  }
 }

 if (type == 1005 || type == 1006) {
  //inject a 1008 message
  mySerial.write(packet1008, 12);
 }
}

Hardware

The base station itself, at present, consists of several components including:

  • 12V to 5V power supply (in future replace with 110v to 5v)
  • ZED-F9P on a SparkFun RTK2 board
  • Arduino Uno
  • Digi 9xTend frequency hopping 1 watt transmitter, using xTend firmware, not mesh

Connecting it together

The ZED-F9P is configured to act as a base station (including either a set latitude and longitude or averaging position using Survey-in), and to output the RTCMv3 messages we desire on UART2.

5v Power is supplied to the 5v rail of all the components. The F9P UART2 wires are connected to the primary serial input of the Arduino, pins 0 and 1. The software serial output pins on the Uno are connected to the Digi 9Xtend radio module.

I used some protoyping shields to make it easier to connect everything in a way that would allow me to easily disconnect parts. The Digi radio is attached to a 20-pin 2mm pitch socket on the first proto board. The power wires and UART connection from the F9P are done on a second prototype shield stacked on top. Originally I used an Ardusimple RTK2 shield instead of the Sparkfun board, which attached on top of that, making a neat stack. When I damaged the Ardusimple board I chose to mount a Sparkfun RTK2 board on a 3d-printed stand next to the original stack. This made a pretty nice clean setup inside the enclosure.

Future plans

In the future I may change the Arduino Uno board out for an ESP32 microcontroller board. This would let me inject the 1008 message, feed the radio, and also send the RTCMv3 feed out the internet via WiFi to an NTRIP Caster.

Links