# Software

## Flow control

### Hot-fire sequence design

The team is required to stay in the bunker during a hot fire, so the engine must be controlled remotely. We chose to use two ESP32s, **one of which would send commands (**[**`COM Board`**](#com-board)**)**, and **the other would report data (`DAQ Board`)**. Both devices were constructed as state machines, with COM Board connected to the computer to transmit the data reported by the DAQ Board.

Here is a state machine diagram to visualize the communication:&#x20;

<figure><img src="/files/w1UXNyAQKRwDKr17X46c" alt=""><figcaption></figcaption></figure>

<details>

<summary>Explanation of the state machine</summary>

After the initial start-up, the COM board first goes into state 0, then settles in state 1. It instructs the DAQ board to enter state 1 and only poll for data at a relatively slow rate (about 1 data point per second). The COM Board and DAQ board lights would flash when a data packet is successfully sent or received.&#x20;

When button 1 is pressed, the COM board enters state 3, the armed state. The DAQ board receives the command to change state and moves to state 3 as well. Both the COM and DAQ lights become solid to indicate the state change. Here the poling occurs more frequently, and the system is ready to execute the hot-fire stages.&#x20;

After button 2 is pressed, ignition occurs. However, no further actions are happening as a visual confirmation of a visible flame is necessary to continue.&#x20;

Once a visible flame is confirmed, and button 3 is pressed, the COM board enters state 5. The DAQ board, after moving to the same state, enters an automatic process. At this point, instead of commanding, the COM board listens for the status update from the DAQ board.

The DAQ board, at state 5, opens GOX valve and keep the ETH valve closed. After approximately 0.5 second, the board moves to state 6, where both valves are opened. State 7 comes x seconds afterward, x being the planned hot-fire time, and closes the GOX valve. It lasts for another 0.5 second, and transitions into state 8, where both valves are closed. At this point, DAQ resets the state to 0, and pass it back to the COM board, which adjusts the state accordingly. A full hot-fire sequence is complete.&#x20;

Devices connected to DAQ: 4 PTs (pressure transducers), 1 FM (flow meter), load cells.&#x20;

</details>

### Code implementation

{% tabs %}
{% tab title="COM Board" %}
{% embed url="<https://github.com/calstar/ellie-ground/blob/main/Aurduino/Dev/COMBoard/COMBoard.ino>" %}

#### Code Highlights

Since the bunkers and the launch pad are approximately 100 ft apart, using a wired cable between the ESP32s, although reliable and still popular in collegiate teams, was not economically efficient. Instead, we established a wireless connection between the COM and DAQ boards, with an information queue that can hold data while transmitting.

{% code title="Define communication structure" overflow="wrap" %}

```cpp
typedef struct struct_message {
    int messageTime;
     int pt1;
     int pt2;
     int pt3;
     int pt4;
     int lc1;
     int lc2;
     int lc3;
     int fm;
    unsigned char S1;
    unsigned char S2;
        int commandedState = 0;
        int DAQstate=0;

    unsigned char I;
    short int queueSize;
    int Debug;
} struct_message
```

{% endcode %}

The "pts" are the pressure transducers we implemented throughout the engine system, while the "lcs" are load cells for measuring the engine thrust, and fm is the flow meter measurement. For both sending and receiving data, functions and interrupts are set up to handle.&#x20;

Establishing the data structure for communications:

<pre class="language-cpp" data-overflow="wrap"><code class="lang-cpp">// Create a struct_message called Readings to recieve sensor readings remotely
struct_message incomingReadings;

// in void setup() we register the for a callback function that will be called when data is received
esp_now_register_recv_cb(OnDataRecv);

void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
 // Serial.print("\r\nLast Packet Send Status:\t");
 // Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");

  if (status == 0){
    success = "Delivery Success :)";
    digitalWrite(INDICATOR2, HIGH);
    receiveTimeDAQ = millis();
  }
  else{
    success = "Delivery Fail :(";
    digitalWrite(INDICATOR2, LOW);
  }

}

// interrupt function, triggered when 
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&#x26;incomingReadings, incomingData, sizeof(incomingReadings));
  incomingPT1 = incomingReadings.pt1;

     digitalWrite(INDICATOR1,HIGH);

// loading the remote data into local variables
  incomingMessageTime= incomingReadings.messageTime;
  incomingPT2 = incomingReadings.pt2;
  incomingPT3 = incomingReadings.pt3;
  incomingPT4 = incomingReadings.pt4;
  incomingFM = incomingReadings.fm;
  incomingLC1 = incomingReadings.lc1;
  incomingLC2 = incomingReadings.lc2;
  incomingLC3 = incomingReadings.lc3;
  incomingS1 = incomingReadings.S1;
  incomingS2 = incomingReadings.S2;
  queueSize= incomingReadings.queueSize;
  incomingI = incomingReadings.I;
  actualState = incomingReadings.DAQstate;
<strong>  incomingDebug= incomingReadings.Debug;
</strong>
// Rest of code omitted
}
</code></pre>

In `setup()`, we need to call the following commands to get everything working:

```cpp
//initialize ESP32
   if (esp_now_init() != ESP_OK) {
  //  Serial.println("Error initializing ESP-NOW");
    return;
  }
   // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
  esp_now_register_send_cb(OnDataSent);

  // Register peer/ second ESP 32
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;

  // Add peer
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
 //   Serial.println("Failed to add peer");
    return;
  }
  // Register for a callback function that will be called when data is received
  esp_now_register_recv_cb(OnDataRecv);
```

The following code is the implementation of the state machine. State cases such as -1, 30, etc. are ommited since they are only for testing and debugging purposes. Those states are not a part of the main hot-fire sequence.&#x20;

<pre class="language-cpp"><code class="lang-cpp"><strong>// State Machine Implementation
</strong>  case (0): //Default/idle
      idle();
      state=1;  
    break;

  case (1): //Polling
    polling();

    if ((digitalRead(BUTTON2)==1)||(serialState==2)) { state=3; S1=servo1ClosedPosition; S2=servo2ClosedPosition; SendDelay=pollingSendDelay; }
    if (serialState==18) {state=18;} 
    if (serialState==17) {state=17;} 
    if (digitalRead(PRESS_BUTTON)==30) {state=30;}

    break;

  case (2): //Manual Servo Control

 manualControl();
  if ((serialState==40)) { state=0; SendDelay=pollingSendDelay; }

  break;

  case (3): //Armed
    armed();

    //button to ignition 
    if ((digitalRead(BUTTON3)==1)||(serialState==3)) { state=4; S1=servo1ClosedPosition; S2=servo2ClosedPosition; SendDelay=ignitionSendDelay; }
    //RETURN BUTTON
    if ((digitalRead(BUTTON1)==1)||(serialState==1)) { state=1; S1=servo1ClosedPosition; S2=servo2ClosedPosition; SendDelay=pollingSendDelay; }
      
    break;


  case (4): //Ignition

    ignition();
    //HOTFIRE BUTTON
      if ((digitalRead(BUTTON4)==1)||(serialState==4)) state=5; 
      //RETURN BUTTON
      if ((digitalRead(BUTTON1)==1)||(serialState==1)) { state=1; S1=servo1ClosedPosition; S2=servo2ClosedPosition; SendDelay=pollingSendDelay; }
      


    break;
  case (5): //Hotfire stage 1

    hotfire();

    if (actualState==0) state=0;


    break;
</code></pre>

{% endtab %}

{% tab title="DAQ Board" %}
{% embed url="<https://github.com/calstar/ellie-ground/blob/main/Aurduino/Dev/DAQBoard/DAQBoard.ino>" %}

Similar to the COM board, the DAQ board also has a data structure for transmitting and receiving informaiton.&#x20;

```cpp
//Structure example to send data
//Must match the receiver structure
typedef struct struct_message {
    int messageTime;
     int pt1val;  int pt2val;  int pt3val;  int pt4val;  int pt5val;  int pt6val; int pt7val;
     int fmval;

    unsigned char S1; unsigned char S2; int commandedState=1; 
    int DAQstate=0;unsigned char I; short int queueSize;
    int Debug;
} struct_message;
```

Sending and receiving:

```cpp
// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("\r\nLast Packet Send Status:\t");
   Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
   if (status ==0){
     success = "Delivery Success :)";
   }
   else{
     success = "Delivery Fail :(";
   }
}

// Callback when data is received
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
  memcpy(&Commands, incomingData, sizeof(Commands));

  S1 =Commands.S1;
  S2 = Commands.S2;


 // UNCOMMENT THIS LATER!!!!!!!!!!!!!!!!
 commandedState = Commands.commandedState;
 // Serial.println(commandedState);
}
```

Like the COM board, it also need to register a "peer." However, unlike COM board, which registers the DAQ board as the peer, DAQ registers COM as the peer&#x20;

\
State machine:&#x20;

```cpp
case (0): //Default/idle
      idle();

      if (commandedState==1) { state=1; MeasurementDelay=pollingMeasurementDelay; }
      if (commandedState==2) { state=2; MeasurementDelay=pollingMeasurementDelay; }
      if (commandedState==3) { state=3; MeasurementDelay=pollingMeasurementDelay; }
      break;

  case (1): //Polling
      polling();

      if (commandedState==0) { state=0; MeasurementDelay=idleMeasurementDelay; }
      if (commandedState==2){  state=2; MeasurementDelay=pollingMeasurementDelay; }
      if (commandedState==3) { state=3; MeasurementDelay=pollingMeasurementDelay; }
      if (commandedState==17) {state=17;} 
      if (commandedState==30) {state=30;}

    break;

  case (2): //Manual Servo Control
    manualControl();
    if (commandedState==0) { state=0; MeasurementDelay=idleMeasurementDelay; }
    if (commandedState==1) { state=1; MeasurementDelay=pollingMeasurementDelay; }
    if (commandedState==3) { state=3; MeasurementDelay=pollingMeasurementDelay; }
    break;

  case (3): //Armed
    armed();
    if (commandedState==0) { state=0; MeasurementDelay=idleMeasurementDelay; }
    if (commandedState==1) { state=1; MeasurementDelay=idleMeasurementDelay; }
    if (commandedState==4) { state=4; igniterTimer=loopStartTime; }
    break;

  case (4): //Ignition

    ignition();
    if (commandedState==0) { state=0; MeasurementDelay=idleMeasurementDelay; }
    if (commandedState==1) { state=1; MeasurementDelay=idleMeasurementDelay; }

    if (commandedState==5) { state=5; hotfireTimer=loopStartTime; MeasurementDelay=hotfireMeasurementDelay; }


    break;
  case (5): //Hotfire stage 1

    hotfire1();

    if ((loopStartTime-hotfireTimer) > hotfireStage1Time) state=6;

    break;

  case (6): //Hotfire stage 2

    hotfire2();
    if ((loopStartTime-hotfireTimer) > hotfireStage2Time) state=7;

    break;

  case (7): //Hotfire stage 3
    hotfire3();
    if ((loopStartTime-hotfireTimer) > hotfireStage3Time) state=8;

    break;

  case (8): //Hotfire stage 4
    hotfire4();
    if ((loopStartTime-hotfireTimer) > hotfireStage4Time){  state=0; MeasurementDelay=idleMeasurementDelay; }

    break;
```

Note that at state 8, state = 0. This change will reset the state on the COM board as well.&#x20;
{% endtab %}
{% endtabs %}

## Automated Instrument Calibration

To measure the thrust force and monitor the internal conditions of the hot-fire system, we needed to collect pressure, flow rate, and force data at various points. Therefore, pressure transducers (PTs), load cells (LCs), and flow meters (FMs) were to be present throughout the engine system. In the first iteration of our liquid engine, we implemented 4 PTs, 3 LCs, and 1 FM. However, those sensors are not plug-and-play devices; they must be calibrated.&#x20;

### Problem

In the past, STAR has done the calibration manually, meaning that we would measure the data using a pressure gauge and weight scale, write it down on a piece of paper (along with the output of the sensors from the electronics), and input the data point to an excel sheet to generate slopes. The slopes, which can be characterized by y = ax+b relationships, would combine with raw data input from the sensors to produce the correct output. This manual process eventually became unsustainable due to two reasons:

* **Time efficiency**. Writing down data, inputting it into an excel sheet, and customizing graphs for different data sets take significant time. Moreover, If the slopes generated do not reflect a linear relationship, as described by the manufacturer, we would have to start the process all over. It delayed our work efficiency, as we could only perform engine tests after all sensors were correctly calibrated.&#x20;
* **Labor efficiency**: manual calibration usually requires two or more people to be present, since the tedious process could easily confuse the operators and result in incorrect inputs. Having an extra pair of eyes would considerably decrease the error rate, but it would take away available members, decreasing the overall work efficiency. In addition, since this process was confusing, it was usually performed by experienced members, who already had numerous other tasks at hand.&#x20;

### Solution

The solution was to create an automated process that could *replace most of the human labor, especially the data logging part where things can easily go wrong*. MATLAB was chosen to be the agent responsible for logging data, calculating relationships, and reflecting them on a graph. This program has an exceptional advantage in matrix operations and data visualization.&#x20;

**The following is an up-shot of how the new process work:**

1. Hook up all the sensors correctly (PTs connected to the same source, LCs experience the same load) and tweak several settings in the MATLAB program if needed. Then flash the Arduino file into the microcontroller for data output.&#x20;
2. Once the correct data stream from the microcontroller is confirmed, start the MATLAB program, wait for several seconds, then click stop.
   * To verify whether the output is correct, use the serial monitor in the Arduino IDE. However, don't forget to close the monitor, since it will occupy the serial connection channel, denying MATLAB's access.&#x20;
3. MATLAB prompts for human input, which is the reading on a pressure gauge or weight scale. After human input, it immediately saves all data, and plots it by the number of devices we wish to calibrate.&#x20;
   * If any data point appears to be a measurement mistake, the operator can open the saved .xls file and delete the corresponding row. However, given that only one human input is prompted each time, all devices must share the same number of data points. Therefore, cell deletion is not allowed; deletion should occur across rows.
4. Repeat steps 2 and 3 until desired relationships are acquired.&#x20;
5. Open the .xls file automatically generated by the program, and locate the first two rows. Each column represents a device. Row 1 is a, and Row 2 is b, assuming y = ax+b.
6. Plug the constants back into the main code to get the correct data output.&#x20;

### Code Implementation

There are two parts to the automation: the .ino file for the microcontroller to output data in the correct format, and the .m file running on the computer that does everything else. Here is the code base:

Note: There isn't a single .m file. Sometimes we wish to calculate more complicated constants, so variations are necessary.  &#x20;

{% hint style="warning" %}
Note: There isn't a single .m file. Sometimes we wish to calculate more complicated constants, so variations are necessary.  &#x20;
{% endhint %}

{% embed url="<https://github.com/calstar/ellie-ground/blob/main/Calibration/Sensor%20Calibration/SensorCalibrator.m>" %}
MATLAB .m file. This is the basic version that can be used for PTs and LCs
{% endembed %}

{% embed url="<https://github.com/calstar/ellie-ground/blob/main/Calibration/Flow%20Meter%20Calibration%20Files/MatLabCdMeasurementCalc/MatlabCdCalc.m>" %}
The .m file for calculating Cd drag coefficient throughout the injector
{% endembed %}

{% hint style="warning" %}
The same principle applies to the .ino files. Here are some of the variations:
{% endhint %}

{% embed url="<https://github.com/calstar/ellie-ground/blob/main/Aurduino/Tests/PTTestForMatlabCalc/PTTestForMatlabCalc.ino>" %}
Basic .ino file for PT and LC calibration
{% endembed %}

{% embed url="<https://github.com/calstar/ellie-ground/blob/main/Calibration/Flow%20Meter%20Calibration%20Files/CdMeasurementReport/CdMeasurementReport.ino>" %}
Cd drag coefficient data output instruction
{% endembed %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://www.hubertyl.com/hubert-lius-porfolio/engineering-related/star-liquid-engine/software.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
