Challyss Model Rocket Booster Test Bench - nRF24L01+ version

Hello all,

I'm a rocket model enthousiast, and I want to share to the community my booster test bench.

RTB - CDU
RTB - MU

Warning : this is a DIY project I did. I didn't hurt myself, but I can't guaranty you won't. I can't be held responsible for any harm you may cause by following these steps.

General idea :

  • Put the booster on the bench (facing down)
  • Use the relay as an ignition switch
  • Start the countdown from your computer
  • Before the countdown ends, measurements start
  • Countdown hits zero, ignition on
  • When the booster is exhausted, stop the measurements
  • Measurement points are acquired by the Measuring Unit (MU)
  • The MU then send the data to the DataToComputer Unit (DCU)
  • The DCU receives the data and send it to the computer's Python User Interface (PUI)
  • The PUI saves the data to a text file
  • ToDo : the PUI graphs the data as it comes
  • Treat the data using excel

BOM MU:

  • (1) Arduino Uno
  • (1) LCD Keypad Shield
  • (1) Red LED
  • (1) 1k resistor
  • (1) HX711 + 10KG load cell
  • (1) nRF24L01+
  • (1) 10nF capacitor
  • (n) Connectors

BOM DCU :

  • (1) Arduino Uno
  • (1) nRF24L01+
  • (1) 10nF capacitor
  • (n) Connectors

Connections MU (ToDo : schematic) :

  • LED/resistor to pin A1
  • HX711 SCK to pin A2
  • HX711 DT to pin A3
  • Relay IN to pin 2
  • HX711 80Hz hack (unsolder 15 and connect to 16 - Google it in doubt)
  • nRF24L01 to SPI ports
  • nRF24L01 CE to pin A5
  • nRF24L01 CSN to pin A4
  • nRF24L01 VCC(3.3v) and GND
  • 10nF capacitor betweet 3.3v and GND

Connections DCU (ToDo : schematic) :

  • nRF24L01 to SPI ports
  • nRF24L01 CE to pin A5
  • nRF24L01 CSN to pin A4
  • nRF24L01 VCC(3.3v) and GND
  • 10nF capacitor betweet 3.3v and GND

ToDo :
Post photos.
Diagrams.
Two ways communication.
Clean code.

Arduino MU code

// Engine Test Bench - Measuring Unit
// Arduino Uno
// nRF24L01+  (RADIO)
// 10nF capacitor
// HX711 + Load Cell (LC)
// Low Level Trigger Relay (FIRE)
// LCD Keypad Shield (LCD)

#include "HX711.h"
#include <LiquidCrystal.h>
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>


#define CDN_TOTAL_DURATION  6000
#define CDN_MES_START       2000 //When CountDowN reaches CDN_MES_START, measurement starts
#define LED_PIN             A1
#define FIRE_PIN            2
#define FIRE_DURATION       1000
#define MES_INTERVAL        25
#define BLINK_INTERVAL_CDN  250
#define BLINK_INTERVAL_MES  50
#define LCD_REFRESH         100

//NRF24L01+ config
#define RADIO_PIN_CE   A5
#define RADIO_PIN_CSN  A4
RF24 radio(RADIO_PIN_CE, RADIO_PIN_CSN);

//Communication constants
#define ETB_DATA_LEN 8  // 4 bytes for (unsigned long)time and 4 bytes for (float)weight
const uint32_t EBT_TIMECODE_START  = 4294967281; //0xFFFFFFF1
const uint32_t EBT_TIMECODE_END    = 4294967282; //0xFFFFFFF2
const byte addrDtC[5] = {'E','T','B','D','C'}; //Engine Test Bench Data to Computer
const byte addrMes[5] = {'E','T','B','M','U'}; //Engine Test Bench Measuring Unit
const byte RADIO_START[8] = {0xF1,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00}; //
const byte RADIO_END[8]  = {0xF2,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00}; //

//LCD config
#define LCD_PIN_RS    8
#define LCD_PIN_EN    9
#define LCD_PIN_D4    4
#define LCD_PIN_D5    5
#define LCD_PIN_D6    6
#define LCD_PIN_D7    7
#define LCD_PIN_BL    10
#define LCD_PIN_INP   A0
LiquidCrystal lcd(LCD_PIN_RS, LCD_PIN_EN, LCD_PIN_D4, LCD_PIN_D5, LCD_PIN_D6, LCD_PIN_D7);

//Load Cell config
#define LC_CALIBRATION  212.5 //HX711 calibration
#define LC_PIN_DOUT     A3
#define LC_PIN_SCK      A2
HX711 scale;

float     inWeight = 0;
uint16_t  inButton = 0;

bool isCDN = false;
bool isMES = false;

uint32_t tCurrent = 0;
uint32_t timeBlinkInterval = 0;
uint32_t timeFireStart = 0;
uint32_t timeStartMES = 0;
uint32_t timeStartCDN = 0;
uint32_t timeLastMES = 0;
uint32_t timeLastBlink = 0;
uint32_t timeLastLCD = 0;
uint8_t  LED_STATE = LOW;


void setup() {
  initRadio();
  initLED();
  initFIRE();
  initLC();
  initLCD();
  radioReadMode();
}

void loop() {
  tCurrent = millis();
  inButton = analogRead(LCD_PIN_INP);
  
  if( (isCDN or isMES) and (radioStopMes() or isLeft(inButton)) ){
    //Finalize measurements
    if(LED_STATE == HIGH){
      blink();
    }
    
    if(isMES or isCDN){
      radioWriteMode();
      radioSendStop();
      radioReadMode();
      isMES = false;
      isCDN = false;
    }
    radioReadMode();
    updateLCD();
    
  }else if( not(isCDN or isMES) ){
    //Initialize Countdown
    if( radioStartMes() ){
      timeStartCDN = tCurrent;
      isCDN = true;
      timeBlinkInterval = BLINK_INTERVAL_CDN;
      blinkStart();
      updateLCD();
    }
    
  }else if(isCDN and not(isMES) ){
    if( isStartMes() ){
      //Initialize measurement
      timeBlinkInterval = BLINK_INTERVAL_MES;
      isMES = true;
      scale.tare();
      tCurrent = millis();
      timeStartMES = tCurrent;
      timeLastMES = tCurrent-MES_INTERVAL;
      radioWriteMode();
      radioSendStart();
      radioReadMode();
      updateLCD();
      tCurrent = millis();
      
    }
  }else if( isCDN and isMES ){
    if( isStopCDN() ){
      //CountDown is zero
      isCDN = false;
      fireStart();
      updateLCD();
    }
  }
  
  if( isBlinkTime() ){
    blink();
    timeLastBlink += timeBlinkInterval;
  }
  
  if( isMESTime() ){
    //Measurement : data acquisition / transmission
    getWeight();
    radioWriteMode();
    radioSendData(tCurrent-timeStartMES,inWeight);
    radioReadMode();
    timeLastMES += MES_INTERVAL;
  }


  if(isStopFire()){
    fireStop();
  }

  if( isLCDTime() ){
    if(not(isMES)){
      getWeight();
    }
    updateLCD();
    
  }
}

void updateLCD(){
  uint16_t lcdCDN = 0;
  uint32_t lcdMES = 0;
  uint8_t i = 0;

  lcd.setCursor(13,0);
  if(isCDN and isMES){
    lcd.print("CDM");
  }else if(isCDN){
    lcd.print("CDN");
  }else if(isMES){
    lcd.print("Mes");
  }else{
    lcd.print("SBY");
  }

  

  if(isCDN or isMES){
    if(isCDN){
      lcdCDN = (uint16_t)((timeStartCDN+CDN_TOTAL_DURATION-tCurrent)/100);//Display CDN
    }else{
      lcdCDN = (uint16_t)((tCurrent-timeStartMES)/100);//Display MES time
    }
    lcd.setCursor(15,1);
    lcd.print(lcdCDN % 10);
    lcdCDN = (uint16_t)(lcdCDN/10);
    lcd.setCursor(14,1);
    lcd.print(".");
    i = 0;
    while(i<3){
      lcd.setCursor(13-i,1);
      if(i>0 and (lcdCDN==0)){
        lcd.print(" ");
      }else{
        lcd.print(lcdCDN % 10);
        lcdCDN = (uint16_t)(lcdCDN/10);
      }
      i++;
    }
  }else{
    lcd.setCursor(11,1);
    lcd.print("     ");
  }

  lcd.setCursor(0,1);
  lcd.print("   '   . g");
  if(inWeight<0){
    lcd.setCursor(0,1);
    inWeight = -inWeight;
    lcd.print("-");
  }
  lcdMES = (uint32_t)(inWeight*10);
  i = 0;
  while(i < 8){
    lcd.setCursor(8-i,1);
    lcd.print(lcdMES % 10);
    lcdMES = (uint32_t)(lcdMES/10);
    i += 1;
    if( (i==1) or (i==5) ){
      i+=1;
    }
  }

  timeLastLCD = tCurrent;
}

void getWeight(){
  inWeight = scale.get_units();
}

bool isMESTime(){
  if(isMES){
    return (tCurrent>(timeLastMES+MES_INTERVAL));
  }else{
    return false;
  }
}

bool isLCDTime(){
  return (tCurrent>(timeLastLCD+LCD_REFRESH));
}

bool isStopFire(){
  if(timeFireStart>0){
    return (tCurrent>(timeFireStart+FIRE_DURATION));
  }else{
    return false;
  }
}

bool isStopCDN(){
  return (tCurrent>(timeStartCDN+CDN_TOTAL_DURATION));
}

bool isStartMes(){
  return (tCurrent>(timeStartCDN+CDN_TOTAL_DURATION-CDN_MES_START));
}

void fireStop(){
  timeFireStart = 0;
  digitalWrite(FIRE_PIN,HIGH);
}

void fireStart(){
  timeFireStart = tCurrent;
  digitalWrite(FIRE_PIN,LOW);
}

bool isBlinkTime(){
  if(isCDN or isMES){
    return ( tCurrent>(timeLastBlink+timeBlinkInterval) );
  }else{
    return false;
  }
}

void blink(){
  if(LED_STATE == HIGH){
    digitalWrite(LED_PIN,LOW);
    LED_STATE = LOW;
  }else{
    digitalWrite(LED_PIN,HIGH);
    LED_STATE = HIGH;
  }
}

void blinkStart(){
  timeLastBlink = tCurrent;
}

bool radioSendData(long timeCode, float weight){
  byte dataTX[8];
  uint8_t timeOut = 5;
  memcpy(&dataTX[0],&timeCode,4);
  memcpy(&dataTX[4],&weight,4);
  while(not(radio.write(&dataTX,8)) and (timeOut>0)){
    timeOut--;
  }
  return (timeOut>0);
}

bool radioSendStart(){
  byte dataTX[8];
  memcpy(&dataTX[0],&RADIO_START[0],8);
  uint8_t timeOut = 10;
  while(not(radio.write(&dataTX,8)) and (timeOut>0)){
    timeOut--;
  }
  return (timeOut>0);
}

bool radioSendStop(){
  byte dataTX[8];
  memcpy(&dataTX[0],&RADIO_END[0],8);
  uint8_t timeOut = 10;
  while(not(radio.write(&dataTX,8)) and (timeOut>0)){
    timeOut--;
  }
  return (timeOut>0);
}

bool isSelect(int val){
  return (val<=800) and (val >= 600);
}

bool isLeft(int val){
  return (val<=599) and (val >= 400);
}

void initLC(){
  scale.begin(LC_PIN_SCK, LC_PIN_DOUT);
  scale.set_scale(LC_CALIBRATION);
  scale.tare();
}

void initLCD(){
  pinMode(LCD_PIN_BL, OUTPUT);
  digitalWrite(LCD_PIN_BL,HIGH);
  lcd.begin(16, 2);
  lcd.setCursor(0,0);
  lcd.print("Load Bench");
}

void initFIRE(){
  pinMode(FIRE_PIN, OUTPUT);
  digitalWrite(FIRE_PIN,HIGH);
}

void initLED(){
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN,LOW);
}

void initRadio(){
  radio.begin();
  radio.setDataRate( RF24_250KBPS );
  radio.setPALevel(RF24_PA_MAX);
  radio.setRetries(1,5); // delay, count
  radio.openWritingPipe(addrDtC);
  radio.openReadingPipe(1, addrMes);
}

bool radioStartMes(){
  if(radio.available()){
    uint8_t messRec[ETB_DATA_LEN];
    uint32_t messRecDecode;
    radio.read(&messRec,ETB_DATA_LEN);
    memcpy(&messRecDecode,&messRec[0],4);
    return (messRecDecode == EBT_TIMECODE_START);
  }else{
    return false;
  }
}

bool radioStopMes(){
  if(radio.available()){
    uint8_t messRec[ETB_DATA_LEN];
    uint32_t messRecDecode;
    radio.read(&messRec,ETB_DATA_LEN);
    memcpy(&messRecDecode,&messRec[0],4);
    return (messRecDecode == EBT_TIMECODE_END);
  }else{
    return false;
  }
}

void radioReadMode(){
  radio.startListening();
}

void radioWriteMode(){
  radio.stopListening();
}

Arduino DTU code

// Engine Test Bench - DataToComputer Unit
// Arduino Uno
// nRF24L01+ (RADIO)
// 10nF capacitor between GND and VCC on the nRF

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

//NRF24L01+ config
#define RADIO_PIN_CE   A5 // I use the same pins on both units, usually these are 7-8 or 9-10
#define RADIO_PIN_CSN  A4 // but I have constraints on the Measuring Unit
RF24 radio(RADIO_PIN_CE, RADIO_PIN_CSN);

//Communication constants
#define ETB_DATA_LEN 8  // 4 bytes for (unsigned long)time and 4 bytes for (float)weight
const uint32_t EBT_TIMECODE_START  = 4294967281; //0xFFFFFFF1
const uint32_t EBT_TIMECODE_END    = 4294967282; //0xFFFFFFF2
const byte addrDtC[5] = {'E','T','B','D','C'}; //Engine Test Bench Data to Computer
const byte addrMes[5] = {'E','T','B','M','U'}; //Engine Test Bench Measuring Unit
const byte RADIO_START[8] = {0xF1,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00}; //
const byte RADIO_END[8]  = {0xF2,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00}; //

// A bunch of variables
unsigned long timeCode;
float         weight;
bool          isListeningRadio = false;
uint8_t       byteFromPy;


void setup() {
    initSerial();
    initRadio();
    radioWriteMode();
}


void loop() {

  if(not(isListeningRadio)){
    if( serialReadStart() ){
      if(radioStartMes()){
        radioReadMode();
        isListeningRadio=true;
      }
    }
  }

  if(isListeningRadio){
    if( serialReadStop() ){
      radioWriteMode();
      radioStopMes();
      radioReadMode();
    }
    if(radio.available()){
      /*  nRF24L01+ has received some data
       *  We copy the first 4 bytes into our time variable
       *  We copy the last 4 bytes into our weight variable
       *  Two special values for time
       *  - 0xFFFFFFF1 : new data (don't worry, it's 47 days)
       *  - 0xFFFFFFF2 : end of data (don't worry, it's 47 days)
       *  The data [time]/t[weight] is sent to the computer through serial
       */
      readRadioData(&timeCode, &weight);
      
      if( isDataStart(timeCode) ){
        sendSerialStart();
      }else if( isDataEnd(timeCode) ){
        sendSerialEnd();
        radioWriteMode();
        isListeningRadio=false;
      }else{
        sendSerialData(timeCode, weight);
      }
    }
  }
}

void readRadioData(unsigned long *timeCode, float *weight){
  uint8_t messRec[ETB_DATA_LEN];
  radio.read(&messRec,ETB_DATA_LEN);
  memcpy(timeCode,&messRec[0],4);
  memcpy(weight,&messRec[4],4);
}

void sendSerialStart(){
  Serial.println("new data");
}

void sendSerialEnd(){
  Serial.println("end");
}

void sendSerialData(unsigned long timeCode, float weight){
  Serial.print(timeCode);
  Serial.print("\t");
  Serial.print(weight);
  Serial.println();
}

bool isDataStart(unsigned long timeCode){
  return (timeCode == EBT_TIMECODE_START);
}

bool isDataEnd(unsigned long timeCode){
  return (timeCode == EBT_TIMECODE_END);
}

void initSerial(){
  Serial.begin(9600);
}

void initRadio(){
  radio.begin();
  radio.setDataRate( RF24_250KBPS );
  radio.setPALevel(RF24_PA_MAX);
  radio.openWritingPipe(addrMes);
  radio.openReadingPipe(1, addrDtC);
}

bool radioStopMes(){
  byte dataTX[ETB_DATA_LEN];
  uint8_t timeOut = 1;
  memcpy(&dataTX[0],&RADIO_END,ETB_DATA_LEN);
  while(not(radio.write(&dataTX,ETB_DATA_LEN)) and (timeOut>0)){
    timeOut--;
  }
  return (timeOut>0);
}

bool radioStartMes(){
  byte dataTX[ETB_DATA_LEN];
  uint8_t timeOut = 1;
  memcpy(&dataTX[0],&RADIO_START,ETB_DATA_LEN);
  while(not(radio.write(&dataTX,ETB_DATA_LEN)) and (timeOut>0)){
    timeOut--;
  }
  return (timeOut>0);
}

void radioReadMode(){
  radio.startListening();
}

void radioWriteMode(){
  radio.stopListening();
}

bool serialReadStop(){
  if(Serial.available()>0){
    byteFromPy = Serial.read();
    return (byteFromPy == 'B');
  }else{
    return false; 
  }
}

bool serialReadStart(){
  if(Serial.available()>0){
    byteFromPy = Serial.read();
    return (byteFromPy == 'A');
  }else{
    return false; 
  }
}

Arduino PUI code
I saved mine as ArdIO.py
The command I use is "ArdIO" or "ArdIO -com n"

import serial
import serial.tools.list_ports
import keyboard
import os.path
import sys

if ("-com" in sys.argv):
    defaultCom = "COM"+sys.argv[sys.argv.index("-com")+1]
else:
    defaultCom = "COM9"

fIndexName = "data_index.txt"
fDataName = "data_"


print("Pythion User Interface (PUI) : ArdIO")
print("Ctrl+Esc to quit.")
print("")

print("Scanning COM ports :")
comList = []
ports = serial.tools.list_ports.comports()
for port, desc, hwid in sorted(ports):
    print("{}: {} [{}]".format(port, desc, hwid))
    if not(desc.find("Arduino")==-1) or not(desc.find("CH340")==-1):
        comList.append([port,desc])

print("") 

nbrCom = len(comList)
if nbrCom == 1:
    arduinoCom = comList[0][0]
    print("Only one Arduino found.")
    print("Listening to serial port : "+arduinoCom)
elif nbrCom == 0:
    print("No device connected...")
    print("Exiting.")
    exit()
else:
    defaultFound = False
    for i in range(nbrCom):
        print(str(i)+" : "+comList[i][1])
        if (defaultCom.lower() == comList[i][0].lower()):
            defaultFound = True
    if defaultFound:
        print("Default port identified : "+defaultCom)
        arduinoCom = defaultCom
    else:
        print("Default port not found.")
        validChoice = False
        while not(validChoice):
            choice=input("Chose a Com port : ")
            if choice:
                if int(choice) in range(nbrCom):
                    validChoice = True
                    arduinoCom = comList[int(choice)][0]

arduino = serial.Serial(arduinoCom, 9600, timeout=.1)
print("Connected.")
print()

#mode :
# 0 : init
# 1 : StandBy for start Sequence
# 2 : Measuring mode (waiting for data or interrupt)
mode = 0 
running = True

while running:
    if (mode == 0):
        print("PUI ready")
        print("[Space] + [Enter] : start sequence")
        print("[Space] + [Escape] : exit PUI")
        print()
        mode = 1
        pressOnce = False
        fileOpened = False
        
        

    elif (mode == 1):
        if keyboard.is_pressed('space+enter'):
            print("Starting sequence")
            print("Waiting for data header")
            print("[Space] + [Shift] : Interrupt sequence")
            arduino.write(b'A')
            mode = 2;
            
        elif keyboard.is_pressed('space+escape'):
            running = False
            print("Terminating.")
            
    elif (mode == 2):
        data = arduino.readline()[:-2]
        if len(data)>0:
            if not(fileOpened) and (data.decode('ASCII')=="new data"):
                if os.path.isfile(fIndexName):
                    f_index = open(fIndexName,"r+")
                    index = int(f_index.read())+1
                    f_index.seek(0)
                    f_index.write(str(index))
                    f_index.close()
                else:
                    f_index = open(fIndexName,"w")
                    f_index.write("0")
                    f_index.close()
                    index = 0
                f_data = open(fDataName+str(index)+".txt", "w")
                print("Creating file : "+fDataName+str(index)+".txt")
                print("Recording...")
                fileOpened = True
            elif fileOpened:
                if (data.decode('ASCII')=="end"):
                    print("Data feed saved to "+fDataName+str(index)+".txt")
                    print("")
                    f_data.close()
                    mode = 0
                else:
                    f_data.write(data.decode('ASCII')+"\n")
                    
        if not(pressOnce) and (keyboard.is_pressed('space+shift')):
            arduino.write(b'B')
            print("Interrupt sequence sent")
            print("")
            pressOnce = True
            if not(fileOpened):
                mode = 0

arduino.close()