Thought It was time to start writing up my project now that I have it working as well as if not better than a commercial system.
Please bear with me as it’ll take a little while to get this written up properly so will be editing it over the next couple of days (weeks?).
Bit of History…
I farm 1200 acres of rolling Lincolnshire Wolds land which was purchased in 2013. I used to work as an IT manager before coming back to my farming roots so I understand technology although I’m not a professional programmer by any means.
GPS…
I have been running two John Deere 1800 screens with 3000 receivers and 200 ATU’s. Both units are about 5 years old and on honesty generally work well. I had enquired about replacing the units for newer touch screens and receivers and was quoted circa £8k per unit installed. This is for the basic starfire reception although it did include section control.
AOG…
I stumbled on AOG about 1 year ago and thought ‘that looks like a good idea’. I finally did some proper reading and research this January / February '21 and decided to build a system for a newly acquired Claas Axion 800 New January '20.
I’ll try and break down my project into sensible modules:
RTK Base Station:
Hardware & Firmware
I bought an Ardusimple simpleRTK2B receiver and Survey GNSS Multiband antenna from Ardusimple. I also acquired a standard ESP32 Development Board.
I flashed the ESP32 with Ardusimples NTRIP Master software. This software is designed for their XBee sized ESP32 board which if you want the easiest life Id suggest purchasing as it requires no guesswork and wiring but it does cost a lot more than a standard ESP32 but we still talking peanuts here.
The Ardusimple Git Hub page has all the resources and instructions you’ll need to get the software onto your ESP32. Read it all here.
The simpleRTK2B was flashed with Ardusimples BASE configuration file. get all the resources and instructions here.
NTRIP Master configuration
When the ESP32 boots up with the NTRIP Master Firmware it’ll create an open access point which you can connect to… connect to it! Then go to the web console which is http://192.168.4.1/
Setup the WiFi Box to connect to your network so that you can gain access to the internet.
Setup UART controller to match the TX & RX pins your using on your board to connect to the simpleRTK2B (not necessary if using XBee ESP32)
Finally put in the account information relating to RTK2Go.com. check next step for setting up an account.
RTK2Go.com
Follow this guide to get a RTK2Go.com account setup and connected to your base station.
Hardware Mounting
The antenna needs a good view of the sky down to about 35 degrees I believe. So mount it somewhere sensibly or if your like me mount it badly with tress blocking it as its near power and internet ;-). To be honest I’ll try and move it to a better position but I haven’t had any problems with it where its currently mounted.
I bought a basic usb phone charger to supply power to the box and mounted it to the wall. Plug usb into the SimpleRTK2B and power the ESP32 from the SimpleRTK2B eg:
XBee 3.3v → ESP32 3.3v
Tractor Setup:
PCBv2
I chose to use the PCBv2 as its what Brian designed so I thought it would keep problems down. All the components are available from DigiKey (even in the UK the shipping only take a couple of days) and the PCB can be printed by JLCPCB and took no longer than 1 week to arrive.
All the information for the PCB including Gerber files can be found in the Support folder on the AOG GitHub.
Now that AOG version 5 is out there you don’t need the MMA but you will need (should use?!) a BNO008x. I happed to get a Sparkfun BNO0080 and it seems to work fine. Ignoring the good advice on this site, I’ve mounted the BNO in the box just under the simpleRTK2b as far away from the motor driver as sensible - seems to work fine.
Assembly of the board is pretty straight forward just remember that the electrolytic capacitors, diodes and IC’s need to be installed in the right direction / pin placement.
There are a couple of modifications on my box which I’ll cover later on.
Steering Wheel
I’ve opted to go with a driven steering wheel. Hydraulic would be cleaner install but I’m not an engineer and I don’t like the idea of it being able to control the tractor by accident (because I’ve installed it incorrectly). There are also other less technical people that drive my tractors and so a mechanical system on the steering wheel makes it easier for them to remember to disengage it when not in use i.e. going on the road. Hydraulic may be more accurate but my solution is pretty dam good to be honest and very rarely do I go more than 4cm of line on rough ground.
I asked the question on here as to whether anyone was using a system on a modern Axion and Stef28 stepped up to the plate with his solution which I blatantly just plagiarised.
The gears are all 3D printed by my tenant who has a fancy 3D printer.
The mechanical part was done by local engineering man up the road from me.
The motor came from Gimson Robotics
Wheel Angle Sensor
I used a Honeywell RTY090LVEAA sensor bought from DigiKey. This is a 90 degree sensor which worked well in my case as tractor manages 30 degree turn in each direction. I paid and engineer to actually install it so that it works and wont fall off!
Photos to come…
Modifications
I had an issue where my tablet only has one USB port. My solution was to use another ESP32 to get the GPS data from the simpleRTK2B and transmit it wirelessly. If you have two USB port use them its a much simpler option. I wrote my own bit of software for the ESP32 which I’ll post at the bottom.(its rough but use if you feel inclined. You’ll have to modify the code to fit your solution.) Easier would be to use mtz8302 solution as is written up better (and works better?)
I have added a current sensor to the setup. This does help with stopping 10amp fuse blowing as the motor is powerful and the cytron motor driver can only do 13amp (I play it safe with 10amp fuse). Just remember to set it to 9 or 10 amps in the AOG configuration or the steering will cut in and out rapidly (took me an hour to work that one out)
Software (AOG) Setup
ToDo
Connectivity
I setup my phone as a WiFi hotspot which the ESP32 handling GPS and NTRIP connects to and the tablet running AOG connect to. This allows communication bewtween the different WiFi devices in the tractor and gives internet to AOG for NTRIP data (from RTK2Go.com.
The Arduinio Nano is connected to the tablet via USB.
Code for ESP32 to transmit GPS over UDP
This also handles NTRIP data from AOG to the SimpleRTK2b.
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiSTA.h>
#include <WiFiUdp.h>
//#include <SoftwareSerial.h>
#include <AsyncUDP.h>
#define LED_BUILTIN 2
//SoftwareSerial SerialNano(4,5);// RX, TX
char ssid[24] = "SSID";
char password[24] = "SSID PASSWORD";
unsigned int GPSSendPort = 9999; //GPS recieving port
unsigned int GPSSendPort2 = 7999; //GPS recieving port for yield monitor
unsigned int GPSSendPortArraySize = 2;//modify this if you modify above line to match number of elements
unsigned int AOGNtripPort = 2232; //Port set in AOG Ntrip UDP
unsigned int MessageSendPort = 9990;
//io pins on first F9P
byte F9P_RX1 = 22;
byte F9P_TX1 = 23;
unsigned int espResetTime = 0;
bool newGPSString = false;// used to sort the serial data comming through
String sGPSMessage = "";//used to collect the gps message
byte ipDestination[4] = { 192, 168, 2, 255 };
//Variables
int lastMessageTime = 0;
int differenceInMessageTime = 0;
//UDP instances
AsyncUDP UdpSender; //create Udp instance for sending.
AsyncUDP UdpSender2; //create Udp instance for sending on second port.
AsyncUDP ntripUdp;
unsigned int ntripPort = 2232;
//WiFiUDP moduleUdp;
void setup() {
//Lets delay 5 seconds to let everything else restart
delay(2000);
Serial.begin(38400); //output to serial terminal USB
while(!Serial){}
delay(500);
//SerialNano.begin(38400);
//delay(500);
//bring up WiFi
initWiFi(ssid, password);
delay(500);
//Serial setups
Serial.println("Waiting before bringing up F9P...");
delay(500);
Serial1.begin(115200, SERIAL_8N1, F9P_RX1, F9P_TX1); //connection to 1st F9P
while(!Serial1){}
Serial.println("Waiting to settle down...");
delay(500);
//pin setups
pinMode(LED_BUILTIN, OUTPUT);//set the blue light
ntripUdp.listen(ntripPort); //start listening for UDP on ntrip port
delay(50); //wait a little
ntripUdp.onPacket([](AsyncUDPPacket packet) //when we recieve a packet
{
for (unsigned int i = 0; i < packet.length(); i++) //loop through the packet
{
Serial1.write(packet.data()[i]); //send each byte to the F9P
//Serial.print(packet.data()[i]);
//Serial.println();
}
});
}
void initWiFi(char ssid[24], char password[24])
{
Serial.println("Connecting to WiFi");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
//Serial.print(".");
// espResetTime = 0;
//espResetTimeLimit = 5;
espResetTime++;
Serial.print("Reset Counter : ");
Serial.println(espResetTime);
delay(1000);
if (espResetTime == 10)
{
//reboot the device
Serial.println("Restarting");
ESP.restart();
}
}
digitalWrite(LED_BUILTIN,HIGH);
Serial.println(WiFi.localIP());
}
void loop()
{
// put your main code here, to run repeatedly:
//check to see if we are connected and if not try to connect again
if(WiFi.status() == WL_CONNECTED)
{
differenceInMessageTime = millis() - lastMessageTime; //only send serial every 10 seconds
//check if suitable time has passed to stop blitzing the serial with messages.
if (differenceInMessageTime > 20000) // every 5 seconds
{
IPAddress myIPAddress = WiFi.localIP();
String myStrIPAddress = String(myIPAddress[0])+ "." + String(myIPAddress[1])+ "." + String(myIPAddress[2])+ "." + String(myIPAddress[3]);
SendOutMessage(1, "WiFi Connected - IP Address : " + myStrIPAddress, false, true);
if (LED_BUILTIN == HIGH) //flash the light
{
digitalWrite(LED_BUILTIN,LOW);
} else {
digitalWrite(LED_BUILTIN,HIGH);
}
lastMessageTime = millis();
}
//try to read from F9P number 1
while (Serial1.available())
{
GetGPSMessage(Serial1.read());
}
}else{// try to reconnect
digitalWrite(LED_BUILTIN,LOW);
Serial.println("WiFi Disconnected. Reconnecting...");
initWiFi(ssid,password);
}
}
void SendOutMessage (int messageType, String sMessage, bool toUDP, bool toSerial)
{
//toSerial == false;
if(toSerial == true){
Serial.println(sMessage);
}
if (toUDP == true){
//send to UDP port
char messageBuffer[sMessage.length()+1]; //byte buffer to hold the message
sMessage.toCharArray(messageBuffer, sMessage.length()+1); //load the message into the buffer
switch(messageType){
case 1: //information
UdpSender.broadcastTo(messageBuffer, MessageSendPort);
break;
case 2: //GPS
UdpSender.broadcastTo(messageBuffer, GPSSendPort);// send GPS message out via UDP
UdpSender2.broadcastTo(messageBuffer, GPSSendPort2);// send GPS message out via UDP for yield monitor
break;
}
}
}
//method to get the GPS NMEA messages from the serial connection
void GetGPSMessage(byte serialByte)
{
if (serialByte == '$') //start of the message
{
newGPSString = true;
}
if (serialByte == 10) //end of the message
{
newGPSString = false;
}
if (newGPSString == true)
{
sGPSMessage.concat((char)serialByte);//add the char to the message
} else
{
sGPSMessage.concat((char)serialByte); //add the carriage return
//check the message starts with a $ and is at least 6 characters long
if ((sGPSMessage.charAt(0) == '$') && (serialByte == 10))
{
//lets do some filtering
//Serial.print(sGPSMessage);
ProcessedMessage(sGPSMessage);
//SendOutMessage(2,ProcessedMessage(sGPSMessage),true,false);//send the message x,y,UDP,SERIAL
}
sGPSMessage = "";//clear the message
}
}
String ProcessedMessage(String sGPSMessage)
{
//only get GGA and VTG messages
String messageHead = sGPSMessage.substring(1,6);
//Serial.println (messageHead);
if((messageHead == "GNGGA") || (messageHead == "GNVTG"))
{
//Serial.println (messageHead);
int stringLen = sGPSMessage.length();
int starLocation = 0;
char nmeaString[stringLen];
long checkSum = 0;
sGPSMessage.toCharArray(nmeaString,stringLen);
//= sGPSMessage;
//Serial.print("nmeaString Length : ");
//Serial.println(stringLen);
for(int i = 0; i <= stringLen; i++){ //
//Serial.print("I : ");
//Serial.println(i);
//Serial.println(nmeaString[i]);
if (nmeaString[i] == '*')
{
starLocation = i;
i = stringLen + 1;
}
if((nmeaString[i] != '$') && (i <= stringLen))
{
//lets checksum
if (checkSum == 0)
{
checkSum = nmeaString[i];
} else{
checkSum = checkSum ^ nmeaString[i];
}
}
}
//pull the check sum from the string
String stringChecksum = sGPSMessage.substring(starLocation+1,sGPSMessage.length()-2);
int receivedChecksumLength = stringChecksum.length();
char receivedChecksumChars[receivedChecksumLength];
stringChecksum.toCharArray(receivedChecksumChars,receivedChecksumLength+1);
long receivedChecksumLong = strtol(receivedChecksumChars, NULL, 16);
if(receivedChecksumLong == checkSum){
SendOutMessage(2,sGPSMessage,true,false);//send the message x,y,UDP,SERIAL
}
}
return sGPSMessage;
}