Here I am providing my wiring and source code for measuring the lift and displaying in in realtime. This setup is simplified since we are not measure drag, just lift. Notice that the measuring station has also been greatly simplified compared to the original design.

Arduino Hardware Wiring Setup
Using the Arduino Uno:
- The load cell that measures the lift is connected to the HX711, which gets its power from the breadboard and sends its data to pin 8 and 9 on the Arduino
- The servo connects to pin 10
- The LCD to pins 11 and 12, and pins 2, 3, 4 and 5. A potentiometer (10K) controls the brightness of the screen. A 220 ohm resistor stands between the LCD (+) and the power bus

Arduino Source Code
#include <HX711.h>
#include <Servo.h>
#include <LiquidCrystal.h>
#define DOUT_LIFT 9
#define SCK_LIFT 8
#define SERVO_PIN 10
HX711 lift_sensor;
Servo servo;
// ----------------------------------
// Servo control parameters
// ----------------------------------
int servoAngleInputCorrection = 0;
int minAllowedAngleInput = 10;
int maxAllowedAngleInput = 110;
// Calibrate servo - use SET ANGLE ___ and read the resulting angle of attack
float servoAngleRef1 = 110;
float attackRef1 = 0;
float servoAngleRef2 = 10;
float attackRef2 = 25;
// ----------------------------------
// Load cell
// ----------------------------------
float calibration_factor_lift = 100;
// ----------------------------------
// Experiment parameters
// ----------------------------------
float incrementAngle = 0.5;
// ----------------------------------
// Other
// ----------------------------------
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
int zeroLiftValue = 0;
float calibration_factor_lift = 100;
// Experiment state variables
bool experimentRunning = false;
bool cellTestRunning = false;
bool cellTestFirstPointCollecting = false;
bool automatedMode = false;
bool valuesHaveChanged = false;
bool servoInitialSet = false;
bool isCalibrating = false;
// Angle settings
float startingAttackAngle = 5;
float currentAttackAngle = 5;
float initialAngleManual = maxAllowedAngleInput;
float servoAngle = initialAngleManual;
float inputAngleSave = minAllowedAngleInput;
float inputAngle = minAllowedAngleInput;
int cycleNumber = 0;
float rollingLift = 0;
struct Measurement {
int lift;
int calibratedLift;
bool isSet;
};
int numberOfPointsInMeasurementArray = 50;
Measurement measurements[50]; // Covers from -10 to 40 inclusive. (for angle of attack)
void setup() {
// Initialize SD card
Serial.begin(57600);
lift_sensor.begin(DOUT_LIFT, SCK_LIFT);
lift_sensor.set_scale(calibration_factor_lift);
servo.attach(SERVO_PIN);
Serial.println("//");
Serial.println("====== STARTING EXPERIMENT - MC5 ======");
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
lcd.print("Lift Value:");
}
void processCommand(String command) {
command.trim();
command.toUpperCase();
if (command.startsWith("SET ANGLE ")) {
float angle = command.substring(10).toFloat();
setAngle(angle);
} else if (command.startsWith("CALIBRATE")) {
isCalibrating = true;
} else if (command.startsWith("UNCALIBRATE")) {
isCalibrating = false;
} else if (command.startsWith("SET ATTACK ")) {
float angleOfAttack = command.substring(11).toFloat();
setAttackAngle(angleOfAttack);
} else if (command.startsWith("SET ANGLEREF1 ")) {
float angle = command.substring(14).toFloat();
setServoAngleRef1(angle);
} else if (command.startsWith("SET ANGLEREF2 ")) {
float angle = command.substring(14).toFloat();
setServoAngleRef2(angle);
} else if (command.startsWith("SET ATTACKREF1 ")) {
float angle = command.substring(15).toFloat();
setAttackRef1(angle);
} else if (command.startsWith("SET ATTACKREF2 ")) {
float angle = command.substring(15).toFloat();
setAttackRef2(angle);
} else if (command.startsWith("SET NEUTRALLIFT")) {
setNeutralLift();
} else if (command == "CELL TEST") {
startCellTest();
} else if (command == "START") {
startExperiment();
} else if (command == "RESET") {
resetExperiment();
} else if (command == "STOP") {
stopExperiment();
} else if (command == "PAUSE") {
pauseExperiment();
} else if (command == "RESUME") {
resumeExperiment();
} else if (command == "READ") {
readValuesFromSensors(true);
} else {
Serial.println("Unknown command: " + command);
}
}
void setAngle(float desiredServoAngle) {
if (!automatedMode && !cellTestRunning) {
errorMessage("Manually set servo angle to " + String(desiredServoAngle));
}
if (desiredServoAngle <= maxAllowedAngleInput && desiredServoAngle >= minAllowedAngleInput) {
inputAngle = desiredServoAngle;
updateServoAngle();
} else {
errorMessage("Servo angle out of allowed range: " + String(desiredServoAngle));
if (desiredServoAngle <= minAllowedAngleInput) {
stopExperiment();
}
}
}
void setAttackAngle(float angleOfAttack) {
float resultingAngle = attackToAngle(angleOfAttack);
setAngle(resultingAngle);
}
float angleToAttack(int angle) {
if (servoAngleRef2 != servoAngleRef1) {
int attackAngle = attackRef1 + (angle - servoAngleRef1) * (attackRef2 - attackRef1) / (servoAngleRef2 - servoAngleRef1);
return attackAngle;
} else {
errorMessage("angleToAttack servoAngleRef2 and servoAngleRef1 are the same. Calibrate again.");
return 0;
}
}
float attackToAngle(float attackAngle) {
if (servoAngleRef2 != servoAngleRef1) {
float angle = servoAngleRef1 + (attackAngle - attackRef1) * (servoAngleRef2 - servoAngleRef1) / (attackRef2 - attackRef1);
return angle;
} else {
errorMessage("servoAngleRef2 and servoAngleRef1 are the same. Calibrate again.");
return 0;
}
}
int roundToSignificantFigures(int num, int n) {
if (num == 0) {
return 0;
}
// Calculate the number of digits in num
double digits = ceil(log10(abs(num) + 0.5));
// Calculate the factor to adjust the number
double factor = pow(10, n - digits);
// Apply the factor to round the number
int rounded = round(num * factor) / factor;
// Return the rounded number
return rounded;
}
void setServoAngleRef1(float angle) {
setAngle(angle);
servoAngleRef1 = angle;
}
void setServoAngleRef2(float angle) {
setAngle(angle);
servoAngleRef2 = angle;
}
void setAttackRef1(float angle) {
attackRef1 = angle;
}
void setAttackRef2(float angle) {
attackRef2 = angle;
}
float getRawLift () {
return lift_sensor.get_units(1);
}
float getLift () {
return getRawLift () - zeroLiftValue;
}
void setNeutralLift() {
zeroLiftValue = int(getRawLift());
errorMessage ("Neutral lift set to " + zeroLiftValue);
}
void displayLiftValueLcd() {
lcd.setCursor(0, 0);
lcd.print("AOA ");
lcd.print(currentAttackAngle);
lcd.print(" ");
float lift_raw = getLift();
lcd.setCursor(0, 1);
lcd.print("Lift ");
lcd.print(roundToSignificantFigures(lift_raw,3));
lcd.print(" ");
}
void resetExperiment() {
currentAttackAngle = angleToAttack(maxAllowedAngleInput);
cycleNumber = 1;
setAttackAngle(currentAttackAngle);
}
void startExperiment() {
if (!automatedMode) {
automatedMode = true;
experimentRunning = true;
resetExperiment();
Serial.println("Experiment started. Angle of Attack: " + String(currentAttackAngle));
} else {
Serial.println("Experiment already running.");
}
}
void startCellTest() {
cellTestRunning = true;
}
void stopExperiment() {
automatedMode = false;
experimentRunning = false;
Serial.println("Experiment stopped after " + String(cycleNumber) + " cycles.");
outputMeasurementsToSerial();
Serial.println("Results were saved to CSV");
}
void pauseExperiment() {
if (automatedMode) {
automatedMode = false;
Serial.println("Experiment paused.");
} else {
Serial.println("Experiment is not running.");
}
}
void resumeExperiment() {
if (!automatedMode) {
automatedMode = true;
Serial.println("Experiment resuming.");
} else {
Serial.println("Experiment is already running.");
}
}
void errorMessage(String msg) {
Serial.println("|> " + msg);
}
void updateServoAngle() {
servoWrite(inputAngle);
}
void outputMeasurementsToSerial() {
Serial.println("Index and Lift only");
for (int i = 0; i < numberOfPointsInMeasurementArray; i++) {
Serial.print(i);
Serial.print(",");
}
Serial.println("");
for (int i = 0; i < numberOfPointsInMeasurementArray; i++) {
Serial.print(measurements[i].lift);
Serial.print(",");
}
}
void outputMeasurements(int attackNow, int servoAngleNow, int lift, bool isCalibratingNow) {
int index = int(attackNow + 5); // Shift by 5 to handle negative indices.
if (cellTestRunning) {
// no output desired in this case
} else {
if (index >= 0 && index < numberOfPointsInMeasurementArray) { // Ensure the index is within bounds.
if (isCalibratingNow) {
// Map attackNow to the array index.
measurements[index].calibratedLift = lift;
measurements[index].isSet = true;
} else {
if (measurements[index].isSet == true) {
measurements[index].lift = lift - measurements[index].calibratedLift;
measurements[index].isSet = true;
} else {
measurements[index].lift = lift;
measurements[index].isSet = true;
}
}
} else {
errorMessage("Out of range");
}
}
}
void readValuesFromSensors(bool outputToSerial) {
float lift_weight = -(lift_sensor.get_units(1) - zeroLiftValue);
float diff = lift_weight - rollingLift;
float diffP = int(1000 * diff / lift_weight) / 10;
rollingLift = lift_weight;
if (outputToSerial) {
outputMeasurements(int(currentAttackAngle), int(servoAngle), int(lift_weight), isCalibrating);
}
}
void runCellTest(float startA, float endA) {
cellTestFirstPointCollecting = true;
currentAttackAngle = startA;
setAttackAngle(startA);
delay(4000);
readValuesFromSensors(true);
setAttackAngle(endA);
currentAttackAngle = endA;
delay(1000);
cellTestFirstPointCollecting = false;
setAttackAngle(startA);
currentAttackAngle = startA;
delay(1000);
readValuesFromSensors(true);
}
void servoWrite (float someAngle) {
servo.write(someAngle);
}
void loop() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
processCommand(command);
}
displayLiftValueLcd();
if (!servoInitialSet) {
servoWrite(initialAngleManual);
servoInitialSet = true;
}
if (cellTestRunning) {
errorMessage("Cell test started");
runCellTest(2.0, 10.0);
runCellTest(5.0, 10.0);
runCellTest(10.0, 10.0);
errorMessage("Cell test ended");
cellTestRunning = false;
} else {
if (experimentRunning) {
if (automatedMode == true) {
currentAttackAngle = currentAttackAngle + incrementAngle;
cycleNumber = cycleNumber + 1;
setAttackAngle(currentAttackAngle);
delay(2000);
}
readValuesFromSensors(true);
delay(500);
}
}
delay(1000);
}
I kept evolving the code and later included an LCD to see in realtime the experiments unfold. I will continue to update this post with new versions of the software and a more precise wiring description.
Check out the other Wind Tunnel posts:
- Part 1: building the test chamber
- Part 2: the diffuser including the fan
- Part 3: the concentrator
- Part 4: the measurement station
- Part 5: the Arduino hardware and software




