Dual prop fan boat motor mixing

Hi, really stuck on this one. Dual propeller fan boat using drone arms, DJI Opto Escs and props. Electrically all works great.
I'm controlling this with Spektrum DX8 transmitter and AR6210 receiver. One radio channel into throttle, the other into aileron.
I'm using a Nano Every to intercept and adjust the radio signal and send corrected version to the escs, like a flight controller. Stopping and moving in a straight line works well, it's the turning that's got me bewildered. I have no gift for the math to taketh from one and giveth to the other. Kindly help my wretched soul if you would, please.

/*
Dual propeller fan boat

Throttle stick on Spektrum DX8 for both motors throttle
trim one or the other motor slower to turn left and right

   TRAVEL %(us)     LOW       CENTER        HIGH

   100              1120       1520          1920
   125              1020       1520          2020
   150              920        1520          2120

   at 2048 resolution (DSMX)
*/
#include <Servo.h>
int rMotorPinIn = 7;
int lMotorPinIn = 9;
Servo rSignalOut;
Servo lSignalOut;
int leftVal = 0; // raw throttle channel in
int rightVal = 0; // raw aileron channel in
int leftValOut = 0; // Arduino adjusted throttle channel
int rightValOut = 0; // Arduino adjusted aileron channel
int leftActualOut = 0; // value actually sent to ESC throttle channel
int rightActualOut = 0; // value actually sent to ESC aileron channel
// variable for Arduino, switch between read signal only mode
// and write ActualOut vals to test the dual brushless motors
boolean readOnlyModeSwitch;

void setup() {
  Serial.begin(115200);
  pinMode (rMotorPinIn, INPUT);
  pinMode (lMotorPinIn, INPUT);
  rSignalOut.attach(8);
  lSignalOut.attach(10);
  // write a signal so props stay stopped for now
  lSignalOut.writeMicroseconds(1000);
  rSignalOut.writeMicroseconds(1000);
  // defaults to write mode, or normal intended usage mode
  readOnlyModeSwitch = false;
  Serial.println("waterMoccasin");
  printMenu();
  Serial.println("");
  Serial.println("Beginning in write (adjusted) mode");
  Serial.println("");
  delay(3000); // wait a bit to read all that
  // clear some lines on monitor screen
  for (int i = 0; i < 5; i++) {
    Serial.println();
  }
}
void loop() {
  if (Serial.available() > 0) {
    char in = Serial.read();
    if ((in == 'r') || (in == 'R')) {
      readOnlyModeSwitch = true;
      Serial.println("read mode");
      Serial.println();
    }
    else if ((in == 'w') || (in == 'W')) {
      readOnlyModeSwitch = false;
      Serial.println("write mode");
      Serial.println();
    }
    else if ((in == 'm') || (in == 'M')) {
      printMenu();
      delay(3000);
    }
  }
  switch (readOnlyModeSwitch) {
    case false: // write mode
      leftVal = pulseIn(lMotorPinIn, 20000);
      rightVal = pulseIn(rMotorPinIn, 20000);
      
      // turn left, spin right faster relative to left
      if  ((rightVal > 1560)&& (leftVal > 1250)) {
        rightVal = (leftVal + rightVal) / 2;
        rightValOut = rightVal;
        leftVal = leftVal - (rightVal / 2);
        leftValOut = leftVal;
      }
      
      // turn right, spin left faster relative to right
      else if ((rightVal < 1440)&& (leftVal > 1250)) {
        leftVal = (leftVal + rightVal + 50) / 2;
        leftValOut = leftVal;
        rightVal = leftVal - (rightVal / 2);
        rightValOut = rightVal;
      }
      
      // both motors straight line or standing still
      else {
        leftValOut = leftVal;
        rightValOut = leftVal;
      }
      // write to setMotors function (do the thing)
      setMotors();
      // show me what ya got
      printRealTimeData();
      break;

    case true: // read pwm only mode
      leftVal = pulseIn(lMotorPinIn, 20000);
      rightVal = pulseIn(rMotorPinIn, 20000);
      printRealTimeData();
      break;
  }
}
void setMotors() {
  // always send your best
  leftActualOut = leftValOut;
  lSignalOut.writeMicroseconds(leftActualOut);
  rightActualOut = rightValOut;
  rSignalOut.writeMicroseconds(rightActualOut);
}
void printRealTimeData() {
  Serial.print("LVal: ");
  Serial.print(leftVal);
  Serial.print('\t');
  Serial.print("RVal: ");
  Serial.println(rightVal);
}
void printMenu() {
  Serial.println("");
  Serial.println("type r for read mode");
  Serial.println("");
  Serial.println("type w for write (adjusted) mode");
  Serial.println("");
  Serial.println("type m to repeat menu");
  Serial.println("");
}

I've got some suggestions for improving and optimizing the code:

  1. Use Meaningful Variable Names:
    Rename variables and comments to make the code more self-explanatory. This makes it easier to understand the code's purpose and functionality.

  2. Remove Magic Numbers:
    Replace the magic numbers (e.g., 1560, 1440) with named constants or variables that represent these values. This makes the code more readable and maintainable.

  3. Simplify Your Turning Logic:
    The turning logic can be simplified. Instead of recalculating the motor values in each case, calculate the average throttle value and adjust it based on the turning direction.

// Calculate the average throttle value
int avgThrottle = (leftVal + rightVal) / 2;

if (rightVal > 1560 && leftVal > 1250) {
  rightValOut = avgThrottle;
  leftValOut = leftVal - (avgThrottle / 2);
} else if (rightVal < 1440 && leftVal > 1250) {
  leftValOut = avgThrottle;
  rightValOut = avgThrottle - (rightVal / 2);
} else {
  leftValOut = avgThrottle;
  rightValOut = avgThrottle;
}
  1. Avoid Duplicate Code:
    The code for reading PWM values and printing real-time data is repeated. You can move this common code outside the switch statement to reduce redundancy and improve maintainability.

  2. Add Comments for Clarity:
    Consider adding comments to explain the purpose of different sections of your code, especially the setup, loop, and functions.

  3. Optimize Your Print Functions:
    Printing data to the serial monitor can be resource-intensive. You can reduce the frequency of printing or optimize your print functions to avoid overloading the serial connection. For example, you might print data only at specific intervals or when certain conditions are met.

Here's the improved code:

#include <Servo.h>

int rMotorPinIn = 7;
int lMotorPinIn = 9;
Servo rSignalOut;
Servo lSignalOut;
int leftVal = 0;
int rightVal = 0;
int leftValOut = 0;
int rightValOut = 0;

void setup() {
  Serial.begin(115200);
  pinMode(rMotorPinIn, INPUT);
  pinMode(lMotorPinIn, INPUT);
  rSignalOut.attach(8);
  lSignalOut.attach(10);
  lSignalOut.writeMicroseconds(1000);
  rSignalOut.writeMicroseconds(1000);
  Serial.println("waterMoccasin");
  printMenu();
  Serial.println("Beginning in write (adjusted) mode");
  for (int i = 0; i < 5; i++) {
    Serial.println();
  }
}

void loop() {
  if (Serial.available() > 0) {
    char in = Serial.read();
    if (in == 'r' || in == 'R') {
      readOnlyModeSwitch = true;
      Serial.println("read mode");
      Serial.println();
    } else if (in == 'w' || in == 'W') {
      readOnlyModeSwitch = false;
      Serial.println("write mode");
      Serial.println();
    } else if (in == 'm' || in == 'M') {
      printMenu();
      delay(3000);
    }
  }

  switch (readOnlyModeSwitch) {
    case false: // write mode
      readPWMValues();
      updateMotorValues();
      setMotors();
      printRealTimeData();
      break;

    case true: // read pwm only mode
      readPWMValues();
      printRealTimeData();
      break;
  }
}

void readPWMValues() {
  leftVal = pulseIn(lMotorPinIn, 20000);
  rightVal = pulseIn(rMotorPinIn, 20000);
}

void updateMotorValues() {
  int avgThrottle = (leftVal + rightVal) / 2;
  
  if (rightVal > 1560 && leftVal > 1250) {
    rightValOut = avgThrottle;
    leftValOut = leftVal - (avgThrottle / 2);
  } else if (rightVal < 1440 && leftVal > 1250) {
    leftValOut = avgThrottle;
    rightValOut = avgThrottle - (rightVal / 2);
  } else {
    leftValOut = avgThrottle;
    rightValOut = avgThrottle;
  }
}

void setMotors() {
  lSignalOut.writeMicroseconds(leftValOut);
  rSignalOut.writeMicroseconds(rightValOut);
}

void printRealTimeData() {
  Serial.print("LVal: ");
  Serial.print(leftVal);
  Serial.print('\t');
  Serial.print("RVal: ");
  Serial.println(rightVal);
}

void printMenu() {
  Serial.println("\ntype r for read mode\ntype w for write (adjusted) mode\ntype m to repeat menu\n");
}

These changes should make your code more readable, maintainable, and efficient.

1 Like

No they can't reverse. Got it working by reassigning both channels in to the throttle stick out and mixing as needed with the other stick. Thanks for your response.

Thanks for all the suggestions, I ended up using a different transmitter since the one I was using had a broken throttle gimbal. Mixing the elevator and aileron just wasn't giving the result I wanted.
The code you posted looks cleaner, cheers for that. On the one I actually use, I take all the Serial printing out anyway since it's no good on the river to me. Final note: anyone else who wanted to use this in the future, I added back in the variable and declaration that somehow got left out in your version. Here's it added back in. Thanks for your help.

#include <Servo.h>

int rMotorPinIn = 7;
int lMotorPinIn = 9;
Servo rSignalOut;
Servo lSignalOut;
int leftVal = 0;
int rightVal = 0;
int leftValOut = 0;
int rightValOut = 0;
boolean readOnlyModeSwitch;

void setup() {
  Serial.begin(115200);
  pinMode(rMotorPinIn, INPUT);
  pinMode(lMotorPinIn, INPUT);
  readOnlyModeSwitch = false;
  rSignalOut.attach(8);
  lSignalOut.attach(10);
  lSignalOut.writeMicroseconds(1000);
  rSignalOut.writeMicroseconds(1000);
  Serial.println("waterMoccasin");
  printMenu();
  Serial.println("Beginning in write (adjusted) mode");
  for (int i = 0; i < 5; i++) {
    Serial.println();
  }
}

void loop() {
  if (Serial.available() > 0) {
    char in = Serial.read();
    if (in == 'r' || in == 'R') {
      readOnlyModeSwitch = true;
      Serial.println("read mode");
      Serial.println();
    } else if (in == 'w' || in == 'W') {
      readOnlyModeSwitch = false;
      Serial.println("write mode");
      Serial.println();
    } else if (in == 'm' || in == 'M') {
      printMenu();
      delay(3000);
    }
  }

  switch (readOnlyModeSwitch) {
    case false: // write mode
      readPWMValues();
      updateMotorValues();
      setMotors();
      printRealTimeData();
      break;

    case true: // read pwm only mode
      readPWMValues();
      printRealTimeData();
      break;
  }
}

void readPWMValues() {
  leftVal = pulseIn(lMotorPinIn, 20000);
  rightVal = pulseIn(rMotorPinIn, 20000);
}

void updateMotorValues() {
  int avgThrottle = (leftVal + rightVal) / 2;
  
  if (rightVal > 1560 && leftVal > 1250) {
    rightValOut = avgThrottle;
    leftValOut = leftVal - (avgThrottle / 2);
  } else if (rightVal < 1440 && leftVal > 1250) {
    leftValOut = avgThrottle;
    rightValOut = avgThrottle - (rightVal / 2);
  } else {
    leftValOut = avgThrottle;
    rightValOut = avgThrottle;
  }
}

void setMotors() {
  lSignalOut.writeMicroseconds(leftValOut);
  rSignalOut.writeMicroseconds(rightValOut);
}

void printRealTimeData() {
  Serial.print("LVal: ");
  Serial.print(leftVal);
  Serial.print('\t');
  Serial.print("RVal: ");
  Serial.println(rightVal);
}

void printMenu() {
  Serial.println("\ntype r for read mode\ntype w for write (adjusted) mode\ntype m to repeat menu\n");
}

Have been doing quite some testing recently to use (mainly) Nano's to control RC vehicles.
In case you're still looking for improvements to your project, happy to share my findings.

For instance, using pulseIn to read the receiver signal is not ideal (it blocks the processor from doing other things). Better to set up an ISR.

// in setup()
attachInterrupt(digitalPinToInterrupt(p_TH), TH_ISR, CHANGE);


void TH_ISR() {
  static unsigned long start = 0;

  if (digitalRead(p_TH) == HIGH) start = micros();
  else {
    pulse[THROTTLE].sample = micros() - start;
    pulse[THROTTLE].newP = true;
  }
}

Implemented a software differential for an rc car, very similar to your required throttle-mix.

  TH_L = (1.0 - factor * (ST / 500.0)) * TH + mid_TH;
  TH_R = (1.0 + factor * (ST / 500.0)) * TH + mid_TH;

Where TH_L and TH_R are the throttle values sent to the ESCs and the strength of the diff is made adjustable with a pot meter, giving value 'factor' (between 0 and 1).

Anyway, happy to share more if you're interested.

1 Like

Been tried before (as most things) and doesn't work as expected and project given away as a bad idea.

https://www.youtube.com/watch?v=4Wtg1qQAp-g

I'm just curious, would it have been possible to mix the throttle and aileron channels in the transmitter? Some radios have programmable mixers, I'm not familiar with your radio, though. (I'm a Futaba guy).

I had a flying setup that did exactly what you describe. I had two electric motors that had differential thrust (rudder channel) mixed with the throttle. It was a flying leaf blower, btw.

1 Like

:grin:Horrible boat, great footage!!!

1 Like

thank you!

I also have used the Nano, have you tried the Nano Every? That's my goto these days, I find it much easier to use, but maybe I'm outdated since I really slowed down on Arduino projects in the last year.

Nano Every is still a great choice, interrupts on every pin, smalll footprint.

When using Arduinos to drive brushless (drone-) motors, I recently did a project using Arduino Nano ESP32 running a DShot implementation to drive BLHeli_S ESCs. Performance is spectacular: very linear, very controllable output and almost silent.

Library used: DShotRMT.h

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.