DEV Community

Cover image for Self-Aligning Satellite Dish in Rust: Final
Ian Ndeda
Ian Ndeda

Posted on

Self-Aligning Satellite Dish in Rust: Final

In this part, we'll be largely tying up loose ends in the application logic.

Table of contents

Requirements

  • 1 x Raspberry Pico board
  • 1 x USB Cable type 2.0
  • 1 x HC-05 Bluetooth module
  • 1 x HMC5833L Compass Module
  • 40 x M-M Jumper Wires
  • 2 x Mini Breadboards
  • 2 x SG90 Servo Motor
  • 1 x PTZ Kit
  • 1 x GPS Module
  • 2 x Relay modules
  • 2 x 4.2V Li-ion Batteries
  • 1 x DC-DC Step-down Converter
  • 1 x Diode

Implementation

Connections

The electrical connections will be the same as from the last part. The magnetometer should now be mounted on the PTZ kit to properly capture the heading of the moving system as shown below.

final-compass-mount

Auto

We are now going to flesh out the auto arm of the application loop by programming for the automatic pan and tilt movements. 

We have already found that compass headings obtained from the magnetometer vary significantly when tilted. Coupling a gyroscope with the magnetometer would be the proper solution to achieve tilt-compensated compass headings.

Our makeshift solution to this problem will be to check whether the auto loop is in its first iteration. If so, the PTZ kit must initially face up before determining the compass heading. This is to avoid readings distorted by tilting.

The AUTO_FIRST_ITER flag is set while toggling from manual to auto.

If, however, the auto loop isn't in its first iteration, the heading will be taken at a tilt and the value compared to adjusted_theta, which is simply the heading after the pan and tilt from the first iteration.

Refer to this algorithm for a clearer explanation.

Replace the Uart tests in our setup area with an announcing message: Self-Aligning Satellite Dish in Rust.

We do not need to print the raw GPS data and coordinates; we can just show the look angles and magnetic heading.

Add the following variables that we'll use.

let mut adjusted_theta: f32 = 0.;// adjusted theta; new theta to track, considers tilt
let mut heading = 0.;// magnetic heading
let mut ref_theta = 0.;// reference theta; initial value to be referenced for variance
Enter fullscreen mode Exit fullscreen mode

In the auto arm after acquiring look angles, add the following to enable the auto alignment of the dish.

// Confirm theta == ref_theta from last iteration
// otherwise go back to first iteration
if theta.round() != ref_theta.round() {
    AUTO_FIRST_ITER.borrow(cs).set(true);
}

if AUTO_FIRST_ITER.borrow(cs).get() {
    // Tilt to face up for accurate heading readings
    tilt_angle = 90.;
    position_kit_tilt(pwm2, &mut tilt_angle);

    delay.delay_ms(250);// wait for tilt kit to arrive at 90 deg

    // Get the magnetic heading
    heading = get_magnetic_heading(&mut i2c0);

    writeln!(serialbuf, "> heading: {} deg.\n> tilt: {} deg.",
             heading.round(), &mut tilt_angle).unwrap();
    transmit_uart_data(
        uart_data.as_mut().unwrap(),
        serialbuf);
} else {
    let tilted_heading = get_magnetic_heading(&mut i2c0);// get the adjusted magnetic heading i.e. at a tilt
    heading = tilted_heading;
    theta = adjusted_theta;// we'll now track adjusted theta since we're at a tilt

    writeln!(serialbuf, "adjusted theta: {} deg.", theta).unwrap();
    transmit_uart_data(
        uart_data.as_mut().unwrap(),
        serialbuf);

    writeln!(serialbuf, "> tilted heading: {} deg.\n> tilt: {} deg.",
             heading.round(), &mut tilt_angle).unwrap();
    transmit_uart_data(
        uart_data.as_mut().unwrap(),
        serialbuf);
}

// Pan moded servo cw while heading != theta i.e.
// Dish is not locked with theta
while !heading_within_range(theta, heading) {
    if pan_clockwise(theta, heading) {
        pan(&mut delay, sio.as_mut().unwrap(), Direction::Cw);

        heading = get_magnetic_heading(&mut i2c0);

        writeln!(serialbuf, "Pan Cw: {} --> {}",
                 heading.round(), theta.round()).unwrap();
        transmit_uart_data(
            uart_data.as_mut().unwrap(),
            serialbuf);
    } else {
        pan(&mut delay, sio.as_mut().unwrap(), Direction::Ccw);

        heading = get_magnetic_heading(&mut i2c0);

        writeln!(serialbuf, "Pan Ccw: {} --> {}",
                 heading.round(), theta.round()).unwrap();
        transmit_uart_data(
            uart_data.as_mut().unwrap(),
            serialbuf);
    }
}
Enter fullscreen mode Exit fullscreen mode

Afterwards clear the GGA_ACQUIRED_FLAG to say we are through.

GGA_ACQUIRED.borrow(cs).set(false);// clear the flag
Enter fullscreen mode Exit fullscreen mode

Position Zero

In the manual arm, we need to write code for position Zero: when the system is generally looking North and facing up.

// Face 0° and look up
tilt_angle = 90.;
position_kit_tilt(pwm2, &mut tilt_angle);

delay.delay_ms(250);// wait for tilt kit to arrive at 90 deg

heading = get_magnetic_heading(&mut i2c0);

theta = 0.;

while !heading_within_range(theta, heading) {
    if pan_clockwise(theta, heading) {
    pan(&mut delay, sio.as_mut().unwrap(), Direction::Cw);

    heading = get_magnetic_heading(&mut i2c0);
    } else {
    pan(&mut delay, sio.as_mut().unwrap(), Direction::Ccw);

    heading = get_magnetic_heading(&mut i2c0);
    }
}

writeln!(serialbuf, "heading: {}   tilt angle: {}", 
     heading.round(), tilt_angle).unwrap();
transmit_uart_data(uart_data.as_mut().unwrap(), serialbuf);
Enter fullscreen mode Exit fullscreen mode

Add the following helper functions:

fn tilt_up(phi: f32, tilt_angle: f32) -> bool {
    let tilt = phi - tilt_angle;

    tilt < 0.
}

fn heading_within_range(theta: f32, heading: f32) -> bool {
    // checks whether heading is +/- 2 of desired pan angle
    heading.round() == theta.round() || heading.round() == theta.round() - 1. 
        || heading.round() == theta.round() - 2.
        || heading.round() == theta.round() + 1.
        || heading.round() == theta.round() + 2.
}

fn pan_clockwise(theta: f32, heading: f32) -> bool {
    // Check if pan sd be cw or ccw 
    let pan = theta - heading;

    if pan.abs() < 180. {
        pan > 0.
    } else {
        pan < 0.
    }
}
Enter fullscreen mode Exit fullscreen mode

Results

Here is the final copy of code after the above algorithm has been implemented.

❗ Do not forget to conduct one last calibration with the mounted magnetometer in its final position. You can load the final code from the examples/compass.rs to facilitate this.

The project is complete! We can now remotely control our model satellite dish via Bluetooth and give it commands to run either in auto or manual modes.

Load the program into the Pico in release mode.

cargo run --release
Enter fullscreen mode Exit fullscreen mode

While in manual mode, giving the CW command has an appropriate effect on the value of the compass heading; the same with the CCW command. 

The Zero command aligns the system North.

final-manual

While in auto mode, the kit aligns to the look angles, starting with a pan until theta is achieved and then a tilt up to phi.

final-auto

The PTZ kit is shown in the demonstration below while in auto mode. The selected GSO satellite is tracked when the entire system is rotated.

final-results

Recommendations.

This project took a naive and straightforward approach in fulfilling its objective. There is a lot of room for improvement. Some low-lying picks are listed below:

  • Introduce the RP2040's watchdog timer.Some of the code we wrote in the project is "blocking," which means that subsequent code must wait until it has finished running. Sometimes these code segments might not finish, freezing the software as a whole. We could use Async Rust or Rust's RTIC framework to write alternative non-blocking programs. We could also use a watchdog timer to break out of these extended loops.
  • Have more functions returning. In case of an error during a run, the function will simply return the error for handling instead of stalling the entire system.
  • Use a gyroscope to complement the magnetometer. This will enable the acquisition of tilt-compensated compass headings. Check this.
  • Implement the entire program as a state machine. 
  • Produce an electrical schematic and PCB for the project.

Top comments (0)