Coded: Hydro Controller Prototype

A continuation from the post Breadboarded: Hydro Controller Prototype, I wanted to spent a bit more time discussing the basic functionality of the controller, and where I would go from here if I were to continue developing this project. You can find the code as it stands on my GitHub repository HydroController, which was the name I had given the project when I first started. Taking a look at the repository, we see the files:

Cayenne.ino
Controller.cpp
Controller.h
Fan.cpp
Fan.h
Heater.cpp
Heater.h
HydroController.ino
ILI9341.ino
Light.cpp
Light.h
NTP.ino
OnOffDevice.cpp
OnOffDevice.h
Pump.cpp
Pump.h
README.md
Starfield.ino
TempSensor.ino
TFT.ino
User_Setup.h

Quickly you’ll see I have separate classes for Fan, Heater, Light, and Pump. Those all derive from the base class OnOffDevice. Before the setup method, we need to declare and initialize the objects and set their default values.

Light light(pin_relay_light,
                    LOW, 
                   "Light",
                   DateTime(2005, 12, 31, 11, 11, 11),
                   TimeSpan(0, 1, 0, 0)
);
Fan fan(pin_relay_fan,
                     LOW,
                    "Fan",
                    DateTime(2005, 12, 31, 11, 11, 11),
                    TimeSpan(0, 1, 0, 0)
);
Pump pump(pin_relay_pump,
                      LOW,
                     "Pump",
                     TimeSpan(0, 0, 1, 0),
                     TimeSpan(0, 0, 1, 0)
);

The problem with this implementation is now we have three hardcoded Object types. We can’t change that at runtime. For instance, if we wanted the fan be Duration based instead of Scheduled based. Which then also leave us to hardcode which implementation to use. And since both Light and Pump all derive from the OnOffDevice base class, we need to split up the implementation for Scheduled and Duration.

// Light
if(light.regulateScheduledTask(now)) {...}

// Fan
if(fan.regulateScheduledTask(now)) {...}

// Pump
if(pump.regulateDurationTask(now)) {...}

Ideally, we’d just iterate over each device and call the regulateTask method, regardless of which type it was at runtime. To allow us to change what is assigned to each channel during runtime, we could use the Strategy design pattern.

Input ins[1];
ins[0].setStrategy(new BinaryInputStrategy(14, INPUT_PULLUP));

Setting input channel 0 to be get treated like a digital input, only attempting to sense two levels. If there was a method named AnalogInputStrategy, we would be able to change the strategy at runtime with:

ins[0].setStrategy(new AnalogInputStrategy(14));

Setting up the structure like this will allow faster changes to be made when scaling up and down the system. We could scale it down for a single input and single output, and all the way up to as many inputs and outputs as memory will allow.

The functional part of the code is checking to see if the current time is before, during, or after the Schedule or Duration based events. The majority of the logic resides in this conditional block in the loop method:

if(controller.isOn() && !controller.isShuttingDown()) {
  regulateTasks();
  Serial.println("");
  Serial.println("");
} else if(!controller.isOn() && controller.isShuttingDown()) {      
  light.turnOff();
  fan.turnOff();
  pump.turnOff();
  resetDurationTimers();
  controller.setShuttingDown(false);
}

So, right away, we see a benefit to the Strategy pattern. The code:

light.turnOff();
fan.turnOff();
pump.turnOff();

Could be replaced with:

for(int i = 0; i < inputs.getCount(); i++) {
	inputs[i].turnOff();
}

Being able to call the turnOff method in a ‘for loop’ means we can have as many inputs as memory or physical pins allow.

Going back to the main conditional block from the loop, we have if the controller is ‘on’ and the not shutting down, we tell each device to regulate their own actions. This allows tremendous flexibility to quickly scale up or down the amount of each type of device. And when new modules are available that require another interface type, one can quickly be added.

For example, say I was working to add the DS18B20 temperature sensor working with the controller. I’d create a new class that implements the InputStrategy strategy, called DallasOneWireInputStrategy.

class DallasOneWireInputStrategy : public InputStrategy
{
  public:
      DallasOneWireInputStrategy();
      DallasOneWireInputStrategy(uint8_t p);
      void getReading();
  private:
      uint8_t pin;	
      OneWire oneWireBus;
      DallasTemperature sensor;

      float getTempCByIndex(int channel);
  protected:
    
};

It was good to attempt the first version so I could see the need to start with the Strategy pattern and build a good fountain. A lot of the programming challenges came from having to include logic to handle Scheduled and Duration based events differently. It would have been great to just tell each device to regulate themselves based on what type is was at runtime.

The code also contains SPIFFS (SPI Flash File Storage built into the ESP32), which give us the ability to store data when powered down, such as current event settings and in the future, WIFI credentials. There was a bit of WIFI code in there from when I was testing it out. And I implemented serial commands which proved to be very helped during the early stages of coding. It really allowed me to test each component with commands targeting one method. I had planned on using NTP to update the RTC when the WIFI was connected to avoid having to manually enter it. There is even a starfield screensaver.

It was good to have a working proof of concept to use moving forward. For the next version, I’d start back over using the SRSv1 project as a template. Move the RTC, SPIFFS, and WIFI code out to their own files. And the TFT code started to quickly become the majority of the code once I got started with it. I should have worked to refactor that and decoupled the TFT code from the application code.

It was a fun project that definitely sharped my analysis, design, engineering, and programming skills. Thank you for following along! If you wanted to continue on to the next post, I move the circuit to a protoboard in, Protoboarded: Hydro Controller. Please check out my recent posts to the right and thanks for following along!

Author: