Self-Aligning Dish in Rust: GPS Example

ian_ndeda

Ian Ndeda

Posted on November 24, 2024

Self-Aligning Dish in Rust: GPS Example

In this part of the series we'll receive raw data from the Neo-6m GPS module and process it to give us readable GPS coordinates.

Create a new directory at the same level as the src directory. Inside this examples directory create a gps.rs file in which we'll write our example program. Copy the final code from the previous part into this file.

Table of Contents

Requirements

  • 1 x Raspberry Pico board
  • 1 x USB Cable type 2.0
  • 1 x Neo-6M GPS Module
  • 9 x M-M jumper wires
  • 1 x HC-05 Bluetooth Module
  • 2 x Mini Breadboards
  • Serial Bluetooth App

Implementation

Connections

Connect the various modules to the Pico as shown below.

gps-example-connection

Raw GPS Data

Modify the super loop by clearing it and checking if the GGA_ACQUIRED flag is set. If so we shall read the received data.

    loop {
        cortex_m::interrupt::free(|cs| {
            let mut uart_data = UARTDATA.borrow(cs).borrow_mut();
            let mut buffer = BUFFER.borrow(cs).borrow_mut();

            if GGA_ACQUIRED.borrow(cs).get() {
                // Transmit raw data from gps
                write!(serialbuf, "\n{}", buffer.as_mut().unwrap()).unwrap(); 
                transmit_uart_data(
                    uart_data.as_mut().unwrap(),
                    &serialbuf);
                GGA_ACQUIRED.borrow(cs).set(false);// clear the flag
            }
        });

        cortex_m::asm::wfi();// wait for interrupt
    }
Enter fullscreen mode Exit fullscreen mode

Flash the program into the Pico.

cargo run --example gps
Enter fullscreen mode Exit fullscreen mode

Open the Bluetooth terminal and flash the program into the Pico with the GPS module connected. We should see the raw GPS data on the terminal every second like so...

gps-raw-results

NB❗ You have to keep the GPS module where it can have a view of the sky. If indoors, please note that it may take a good while to lock onto satellites.

Continuous blinks every second is an indication of the module acquiring sufficient GPS data.

GPS Coordinates

We should now process the raw GPS data received into location coordinates. This can be achieved by implementing the below algorithm.

gga-parse

Before we implement the algorithm we need to create a Position struct to hold the dish's coordinates.

struct Position {
    lat: f32,
    long: f32,
    alt: f32,
}

impl Position {
    fn new() -> Self {
        Position {
            lat: 0.,
            long: 0.,
            alt: 0.,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the main section we'll declare and initialize our position.

let mut position = Position::new();// gps position struct
Enter fullscreen mode Exit fullscreen mode

We can now implement the parsing algorithm.

use heapless::Vec;

fn parse_gga(buffer: &mut String<164>, position: &mut Position) {
    //  Updates position of earth station
    let v : Vec<&str, 84> = buffer.split_terminator(',').collect();

    if !v[2].is_empty() {// latitude
        let (x, y) = v[2].split_at(2);

        let x_f32 = x.parse::<f32>().unwrap_or(0.); 

        let y_f32 = y.parse::<f32>().unwrap_or(0.); 

        if x_f32 + (y_f32/60.) != 0. {// if reading valid i.e. != 0 update position
            position.lat = x_f32 + (y_f32/60.);

            if v[3] == "S" {
                position.lat *= -1.;
            }
        }

        if !v[4].is_empty() {
            let (a, b) = v[4].split_at(3);

            let a_f32 = a.parse::<f32>().unwrap_or(0.); 

            let b_f32 = b.parse::<f32>().unwrap_or(0.); 

            if a_f32 + (b_f32/60.) != 0. {
                position.long = a_f32 + (b_f32/60.);// if reading valid i.e. != 0 update position

                if v[5] == "W" {
                    position.long *= -1.;
                }
            }

            if !v[9].is_empty()  {
                let c = v[9];
                position.alt = match c.parse::<f32>() {
                    Ok(c) => {
                        //Put LED indicating valid GPS data ON
                        //unsafe { *SIO_GPIO_OUT |= 1 << 25 };
                        //sio.gpio_out.modify(|r, w| unsafe { w.bits(r.gpio_out().bits() | 1 << 25)});
                        c
                    },
                    Err(_) => position.lat,// replace w/ last position
                };
            }
        }
    } 
}
Enter fullscreen mode Exit fullscreen mode

Update the main loop to update and transmit GPS coordinates via Serial Bluetooth App. Add the following code below the one from the previous section.

// update the coordinates from the nmea sentence
parse_gga(buffer.as_mut().unwrap(), &mut position);

// Transmit gps position 
let lat = position.lat;
let long = position.long;
writeln!(serialbuf, "lat: {}  long: {}", lat, long).unwrap();
transmit_uart_uart_data(
    uart_data.as_mut().unwrap(),
    serialbuf);
Enter fullscreen mode Exit fullscreen mode

Update our main loop to update and transmit GPS coordinates.

Flash the program.

cargo run --example gps
Enter fullscreen mode Exit fullscreen mode

The GPS coordinates should now be displayed on the terminal

gps-coord

Look Angles

We can further calculate for look angles using the below function which implements a satellite look angle calculator.

fn get_look_angles(lat: f32, long: f32, alt: f32) -> (f32, f32) {
    // Earth Station in radians
    let le: f32 = lat*PI/180.; // Latitude: N +ve, S -ve??
    let ie: f32 = long*PI/180.; // Longitude: E +ve, W -ve??
    let _h: f32 = alt; // Altitude

    // Satellite position in radians
    const _LS: f32 = 0.;// Latitude
    const IS: f32 = 50.*PI/180.;// Longitude

    // Determine Differential Longitude, b
    let b = ie - IS;

    let mut theta = (((le.cos()*b.cos())-0.151)/(1.-(le.cos()*le.cos()*b.cos()*b.cos())).sqrt()).atan();
    let mut ai = (b.tan()/le.sin()).atan();

    // Convert into Deg.
    theta *= 180.0/PI;
    ai *= 180./PI;

    // Azimuth Angle
    let phi_z = {
        if (le < 0.) && (b > 0.) {
            360. - ai
        } else if (le > 0.) && (b < 0.) {
            180. + ai
        } else if (le > 0.) && (b > 0.) {
            180. - ai
        } else {
            ai
        }
    };

    (theta, phi_z)
}
Enter fullscreen mode Exit fullscreen mode

Import PI from the core library that'll be used in the get_look_angles function.

use core::f32::consts::PI;
Enter fullscreen mode Exit fullscreen mode

Also import the micromath crate to facilitate the function.

use micromath::F32Ext;
Enter fullscreen mode Exit fullscreen mode

In the toml file under dependencies add:

micromath = "1.1.1"
Enter fullscreen mode Exit fullscreen mode

Update the loop section to calculate the look angles and transmit via uart0.

// Calculate look angles
(theta, phi) = get_look_angles(
    position.lat,
    position.long,
    position.alt);

writeln!(serialbuf, "theta: {}  phi: {}", theta, phi).unwrap();
transmit_uart_uart_data(
    uart_data.as_mut().unwrap(),
    serialbuf);
Enter fullscreen mode Exit fullscreen mode

We will the continue with initializing both theta and phi in the set-up section.

    // Variables
    let (mut theta, mut phi) = (0., 0.);
Enter fullscreen mode Exit fullscreen mode

Results

The final code will be this.

Highlights of changes made are here.

Flashing this into the Pico, we should be able to receive the raw GPS data from the Neo-6m module and calculate look angles.

gps-example-final

In the next part we continue with the application of the above in our main program.

💖 💪 🙅 🚩
ian_ndeda
Ian Ndeda

Posted on November 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related