The code is hosted on GitHub, here: https://github.com/freeheel3r/ufcs-esp32
First, a few words about the repository structure.
One handy advantage of the ESP32 is that it (partially) supports Arduino libraries. This makes it quite a bit more accessible to people who are unfamiliar with microcontrollers, and was one of the reasons why I chose to use it for this project. It also makes it possible to use C++. As you will notice if you go through the code, I am quite fond of object-oriented programming, and have made an effort to compartmentalize the code as much as possible, separating logical units of it into different classes.
Programming the ESP32 is made even easier through the use of PlatformIO, an IDE that handles all the complicated business of downloading the correct toolchains, managing libraries, and building and uploading code with minimal effort. This is why the repository has a "lib" folder and a "platformio.ini" file in its root. All of the actual code is in "src" and consists of just a few files which are explained below.
If you are familiar with the Arduino way, you will know that all programs, or sketches, center around two functions: the setup() function, which is executed once when the microcontroller starts up, and the loop() function, which is executed over and over. While this is the case here, we actually offload all the functionality to the Controller class. This class has its equivalent of setup and loop, and has member variables to keep track of all information that must persist across calls to loop. This is a neater solution than the usual Arduino way of declaring global variables in the main sketch file.
So, the Controller class is the main class; it handles all the top-level functionality, as well as serial communication (which is very simple, and handled by 2-3 short functions).
The microcontroller can receive a few simple instructions from the computer: either a request for the status of a given component (say, the current pressure recorded by pressure controller number two), or a request to set a component to a given state. For example, switch pump 2 off, open valve 12, or set pressure 1 to 5 PSI. In these latter cases, the request is very similar for all components, and the general behavior of the microcontroller is the same (set this component to that value). However, the details of how to accomplish that will vary from one component to the next. For example, opening a valve will require setting a given I/O pin either to HIGH (+3.3V) or LOW (GND); while setting a pressure will require setting a certain analog voltage on another pin.
Therefore, to separate the high-level logic from the implementation, the valves, pumps, and pressure controllers are each represented by a different class, all of which inherit from the same base class. This base class presents all that the controller needs: a function to set a certain state, and a function to retrieve the current state. The derived classes then implement these functions in different ways, depending on the specific components.
This way, when the Controller receives a request to set Component X to value Y, it simply calls X's setValue function, passing it Y, and lets X handle the specifics. It doesn't need to know whether X is a valve or pump, or what Y means and how to translate it.
All the hard-coded constants, such as the ones indicating which component is connected to which IO pin, are found in the constants.h file.