Controlling a portable AC

Discuss garden automation systems and software here, including commercial products or Raspberry Pi and Arduino DIY setups.
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

I just drove myself crazy for an hour trying to figure out why I couldn't get temperature values from the probe with a MAX31855 over the SPI bus. Turns out this AC unit doesn't use Type K thermocouples, they're thermistors. I need a voltage divider and the analog input pin instead. :roll:
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Power's sorted out, I hope. This AC unit comes with a 110->12VAC power transformer. I'll keep it and from that get 12VDC. That will feed the 12V relay coils and also from 12VDC another adapter makes 5VDC for the esp32.
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

It's snug. Trying to wire tuck and tidy it up.
IMG_20220110_221214.jpg
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Baby steps.
IMG_20220112_100214.jpg
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Previously pasted source code outdated, since then have added OTA support, flood flapper detection, more diagnostic mqtt commands, etc. Both fans and the compressor are functional, coil temperature readings are coming in. When the compressor runs, one coil warms and the other cools. I'm psyched it's functional. It's not very stealthy and clearly oversized for my needs, at least now I'm not too worried about monitoring the compressor's duty cycle. And the duct tape has already started coming off so I'll do the obvious solution and add more duct tape.
IMG_20220113_230318.jpg
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

So it's now wired up and plumbed into my grow. It's not really what I was expecting. In order to dehumidify, the evaporator coil needs to be so cold that it's affecting the grow air temperature and the heater has to run more to compensate.

Obviously the AC unit I'm using is an AC first and dehumidifier second. It makes great cold air, but it's a mediocre dehumidifier due to the heat from the condenser not being recirculated back into the grow.

Naturally, I ordered a proper dehumidifier (GE ADEW20LY) and will move my circuitry into that unit instead.
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Chapter two! Or what should have been chapter 1 in the first place but I was making do with what I had on hand.

New dehumidifier arrived today:
Resizer_16431707619050.jpg
Resizer_16431708378610.jpg
Right away, I notice you can only set the desired humidity in 5% increments, not very precise.

Let's strip it naked:
Resizer_16431707619053.jpg
Resizer_16431707619054.jpg
So this is less obvious on how to get it plumbed into the air handling ducts. The white body plastic is actually part of the squirrel cage fan enclosure, I will need to keep that fan enclosed to make it work.

A proper dehumidifier with the evaporator and condenser coils sandwiched together:
Resizer_16431720298370.jpg
The circuit is different:
Resizer_16431707619052.jpg
Resizer_16431719933781.jpg
Resizer_16431721157890.jpg
On the AC unit, the circuit board within the guts had the microcontroller on it, while the display & buttons panel was more of a remote control with no electronics on it. On this dehum, the display board has the microcontroller while the main board is just relays and power. There's a 12V AC power transformer once again, relays are 12VDC. This dehum has two thermistors, one is attached to the evaporator coil like on the AC unit, but another is attached to the outlet of the compressor, probably to make sure nothing overheats.

This time around, I'm thinking that I will leave the main board alone, and instead figure out the pinout for the cable connecting the main board to the display board in order to replace the display board with the usual esp32...
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Well that's convenient, not only is the pinout printed but there's also 5V provided.
153.jpg
3 speed fan, compressor, and an input from the reservoir floater.
Shimbob
LED Wizard
LED Wizard
Reactions:
Posts: 642
Joined: Mon Nov 27, 2017 11:29 pm

Waiting on the delivery of a 4-channel optoisolator...

Fingers crossed that this code is just right. I still need to figure out what kind of hydrometer came with the dehumidifier, I decided to read its values just for the sake of completion, even though whether the dehum runs or not is still based on the canopy humidity sensor. I also noticed the two thermistors had vastly different resistance despite being near the same temperature so I might need to tweak the readTherm function.

Code: Select all

// espDehum
// control a dehumidifier
// v20220129
//BUGS mqtt payload have detritus from previous payloads?

//this device
#define espName "espDehum"
int interval = 1000; //loop interval ms
unsigned long lastLoop=0;
#define LEDGPIO 25
#include <ArduinoOTA.h>

//#define DEBUG
//#define DIAG

//For display on Wifi Kit 32
#include "heltec.h"
char line[64];

//WIFI Stuff
#include <WiFi.h>
const char* ssid = "***";
const char* password = "***";
IPAddress   staticIP(192,168,2,55);
IPAddress   gateway(192,168,0,1);
IPAddress   subnet(255,255,0,0);

//MQTT Client stuff
#include <PubSubClient.h>
WiFiClient    espClient;
PubSubClient  mqtt(espClient);
const char*   mqtt_server = "192.168.2.1";
char          tempString[8];
unsigned char topic[32];
unsigned char payload[64];

//HeatPump stuff
class Dehumidifier {
private:
//thermistor consts
  #define A (0.001129148)
  #define B (0.000234125)
  #define C (0.0000000876741)
  #define R2 (103.1*1000)
  #define VCC (3.29)

// GPIOs esp32 34-39 input only
  //OUTPUTS
  #define slowFanGPIO 13
  #define mediumFanGPIO 12
  #define fastFanGPIO 14
  #define compressorGPIO 27
  //INPUTS:
  #define hydroGPIO 39 //?
  #define floaterGPIO 38
  #define dischargeGPIO 37
  #define tubeGPIO 36

// Internal
  double tubeTemp = 0;
  double tubeTargetTemp = 0;
  double dischargeTemp = 0;
  double dischargeMax = 100; //100C seems like a good start //todo: subscribe to setDischargeMax
  double airTemp = 0;
  double airHumi = 0;
  double Humi = 0;
  double h=0, airDew=0, dewCalibration = -7.5;
  bool compressor = false;
  char fan = 0, newfan = 0; // 0 = off, 1 = slow, 2 = med, 3 = fast
  unsigned long pumpStopTime = 0;
  boolean floater = false;
  boolean overheat = false;
  unsigned long coolDownTime = 1000*1; //store as millis
//Coil Temperature
  #define samples 3
  unsigned int ADC;
  int ADCIndex=0, i;
  double Vout, Rth;

  typedef enum { // HP_State_t
    none = 'n',
    idle_State = 'i',   //Idle...
    fan_State = 'f',    //just the fan
    dehum_State = 'd',     //pump the cool
    pumpCool_State = 'p', //cool the pump
    floater_State = 'F', //condensate accumulating
    overheat_State = 'O' //todo
  } HP_State_t;

  typedef enum { // HP_Event_t
    noneEvent = 'n',
    SetFan = 'S',
    StopFan = 's',
    StartDehum = 'D',
    StopDehum = 'd',
    Floater = 'f',
    Overheat = 'o'
  } HP_Event_t;

  HP_State_t myState=none, nextState=idle_State;
  HP_Event_t myEvent=noneEvent;

  inline void readHydro() {
    //todo 
    Humi = millis() % 100; //some random value for now
  }
  
  inline void readTherm(int GPIO, double *temp) {
    ADC = 0;
    
    for (i = 0; i < samples; i++) {
      ADC += analogRead(GPIO);//not paying attention to overflow, this could be smarter
      delay(1);
    }
    
    ADC /= samples;

    #ifdef DEBUG
      Serial.print("ADC: ");Serial.println(ADC);
      dtostrf(ADC, 4, 1, tempString);
      mqtt.publish("espDehum/ADC", tempString);
    #endif

    Vout = (ADC * VCC) / 4095;
    #ifdef DEBUG
      Serial.print("VOUT: ");Serial.println(Vout);
    #endif

    Rth = (VCC * R2 / Vout) - R2;
    #ifdef DEBUG
      Serial.print("Rth: ");Serial.println(Rth);
    #endif

    Rth = log(Rth);

    *temp = (1 / (A + (B * Rth) + (C * Rth*Rth*Rth))) - 273.15;

  }
  
  inline void gettubeTemp() {

    readTherm(tubeGPIO, &tubeTemp);
    
    #ifdef DEBUG
      Serial.print("tubeTemp: "); Serial.println(tubeTemp);
    #endif

      dtostrf(tubeTemp, 4, 1, tempString);
      mqtt.publish("espDehum/tubeTemp", tempString);
  }

  inline void getdischargeTemp() {

    readTherm(dischargeGPIO, &dischargeTemp);
    
    #ifdef DEBUG
      Serial.print("dischargeTemp: "); Serial.println(dischargeTemp);
    #endif

    dtostrf(dischargeTemp, 4, 1, tempString);
    mqtt.publish("espDehum/dischargeTemp", tempString);

    overheat = dischargeTemp > dischargeMax? true : false;
    
    if(overheat) {
      mqtt.publish("espDehum/alert", "Overheat!");
      myEvent=Overheat;
    }
  }

  inline void getFloater() {
    //pull up, LOW on floater is up
    floater = !digitalRead(floaterGPIO);

    if(floater) {
      mqtt.publish("espDehum/alert", "Floater!");
      myEvent=Floater;
    }
  }

  inline void setAirHumi(double n) {
    airHumi = n;
    //Serial.println("setAirHumi");
    calcDew();
  }

  inline void setAirTemp(double n) {
    airTemp = n;
    //Serial.println("setAirTemp");
    calcDew();
  }

  inline void calcDew() {
    h = (log10(airHumi)-2)/0.4343 + (17.62 * airTemp)/(243.12+airTemp);

    airDew = 243.12*h/(17.62-h);
    dtostrf(airDew, 4, 1, tempString);
    mqtt.publish("espDehum/airDew", tempString);
    //Serial.print("pub:");Serial.println(tempString);
  }

  inline void calcTargetTemp() {
    //set target coil temp to dewpoint+calibration
    tubeTargetTemp = airDew + dewCalibration;
    //Serial.println(coilTargetTemp);
  }

  inline void stopFan() {
    digitalWrite(slowFanGPIO, LOW);
    digitalWrite(mediumFanGPIO, LOW);
    digitalWrite(fastFanGPIO, LOW);
  }

  inline void setFan(int newfan) {

    if(fan) stopFan();
    
    fan = newfan;
    
    switch(fan) {
      case 0:
        break;
      case 1:
        digitalWrite(slowFanGPIO, HIGH);
        break;
      case 2:
        digitalWrite(mediumFanGPIO, HIGH);
        break;
      case 3:
        digitalWrite(fastFanGPIO, HIGH);
        break;
    }
  }

  inline void startPump() {
    if(millis() - pumpStopTime > 3000) {
      digitalWrite(compressorGPIO, HIGH);
      compressor = true;
    }
    else {//dont start pump if it's only been X seconds since last turned off
      #ifdef DEBUG
        Serial.println("Delaying start");
      #endif
      }
  }

  inline void stopPump() {
    digitalWrite(compressorGPIO, LOW);
    compressor = false;
    pumpStopTime = millis();
  }

  inline void startDehum() {
    nextState = dehum_State;
  }

  inline void screenPreLoop() {
    Heltec.display->clear();

    sprintf(line, "Stt: %c  Evnt: %c  ntrvl: %i", myState, myEvent, interval);
    Heltec.display->drawString(0, 0, line);

    sprintf(line, "T:%2.f H:%2.f D:%2.f T:%2.f D:%2.f", airTemp, airHumi, airDew, tubeTemp, dischargeTemp);
    Heltec.display->drawString(0, 10, line);
    Heltec.display->drawString(0, 20, String("Hydro: ") + Humi);
    Heltec.display->drawString(0, 30, String("tubeTarg: ") + tubeTargetTemp + String(" dewCa:") + dewCalibration);
    Heltec.display->drawString(0, 40, String("DischargeTemp:") + dischargeTemp);
    Heltec.display->drawString(0, 50, String("F:") + fan  +String(" CO:")+ compressor + String("   FL:") + floater); //String("SF:") + slowFanGPIO + String(" MF:")+mediumFanGPIO + String(" FF:")+fastFanGPIO
  }

  inline void screenPostLoop() {
    Heltec.display->display();
  }

public:

  void callback(char* topic, byte* payload, unsigned int length) {
    if (0 == strcmp(topic, "canopy/humidity"))  
      setAirHumi(atof((char *)payload)); //no sanity checking

    else if (0 == strcmp(topic, "canopy/temperature"))
      setAirTemp(atof((char *)payload)); //no sanity checking

    else if (0 == strcmp(topic, "espDehum/setFan")) {
      newfan = payload[0]; //no sanity checking
      myEvent = SetFan;
    }

    else if (0 == strcmp(topic, "espDehum/dehum")) {
      if(payload[0] == '1')
        myEvent = StartDehum;
      else if (payload[0]=='0')
        myEvent= StopDehum;
      else
        Serial.println("unhandled payload");}

    else if (0 == strcmp(topic, "espDehum/setCoolDown")) {
      coolDownTime = 1000*atoi((char *)payload); //no sanity checking here, payload in seconds, stored as millis
      #ifdef DEBUG
        Serial.print("coolDownTime: "); Serial.println(coolDownTime);
      #endif
    }

    else if (0 == strcmp(topic, "espDehum/setDewCalibration")) {
      dewCalibration = atoi((char *)payload); //no sanity checking here, payload in Temp C
      #ifdef DEBUG
        Serial.print("dewCalibration: "); Serial.println(dewCalibration);
      #endif
    }

#ifdef DIAG
    else if (0 == strcmp(topic, "espDehum/sfan")) { //diagnostic
      if(payload[0] == '1')
        setFan(1);
      else if (payload[0]=='0')
        setFan(0);
      else
        Serial.println("unhandled payload");}

    else if (0 == strcmp(topic, "espDehum/mfan")) { //diagnostic
      if(payload[0] == '1')
        setFan(2);
      else if (payload[0]=='0')
        setFan(0);
      else
        Serial.println("unhandled payload");}

    else if (0 == strcmp(topic, "espDehum/ffan")) { //diagnostic
      if(payload[0] == '1')
        setFan(3);
      else if (payload[0]=='0')
        setFan(0);
      else
        Serial.println("unhandled payload");}

    else if (0 == strcmp(topic, "espDehum/comp")) { //diagnostic
      if(payload[0] == '1')
        startPump();
      else if (payload[0]=='0')
        stopPump();
      else
        Serial.println("unhandled payload");}

    else if (0 == strcmp(topic, "espDehum/reboot")) {
      stopPump();
      setFan(0);
      esp_restart();
    }

#endif

    else {
      Serial.print("Unhandled callback topic: ");
      Serial.println(topic);
    }

    memset(payload, 0 , length); //wtf buggy mqtt library not clearing previous payload?
}

  Dehumidifier() {
    pinMode(slowFanGPIO, OUTPUT);
    pinMode(mediumFanGPIO, OUTPUT);
    pinMode(fastFanGPIO, OUTPUT);
    pinMode(compressorGPIO, OUTPUT);
    
    pinMode(tubeGPIO, INPUT);
    pinMode(dischargeGPIO, INPUT);
    pinMode(floaterGPIO, INPUT);

    digitalWrite(slowFanGPIO, LOW);
    digitalWrite(mediumFanGPIO, LOW);
    digitalWrite(fastFanGPIO, LOW);
    digitalWrite(compressorGPIO, LOW);

    //todo NVRAM to resume state after unexpected poweroff?
    //Serial.println("setup()'d");

    }

  void loop() {
    readHydro();
    
    gettubeTemp();
    
    getdischargeTemp();
    
    getFloater();

    myState = nextState;

    screenPreLoop();

    //Serial.print("loop: myState: "); Serial.println((char)myState);

    switch(myState) {

      case none:
        break;

      case idle_State:
        switch(myEvent) {
          case Floater:
          case Overheat:
            //what do we do? stall for now
            break;

          case SetFan:
            setFan(newfan);
            nextState = fan_State;
            Serial.println("Idle State, Start Fan");
            break;

          case StartDehum:
            Serial.println("Idle State, Start Dehum");
            startDehum();
            break;

          case StopDehum:
            //ignore
            break;

          case noneEvent:
            //Serial.println("Idle, no event");
            break;

          default:
            Serial.print("Unhandled event: "); Serial.print((char)myEvent); Serial.print(" in state: "); Serial.println((char)myState);
            break;
        }
        break;

      case fan_State:
        switch(myEvent) {
          case Floater:
          case Overheat:
            //what do we do?
            break;

          case noneEvent:
            //Serial.println("Fan State, no event");
            break;

          case StopFan:
            setFan(0);
            nextState = idle_State;
            Serial.println("Fan State, StopFan event");
            break;

          case StartDehum:
            startDehum();
            break;

          case StopDehum:
          //do nothing
            break;

          default:
            Serial.print("Unhandled event: "); Serial.print((char)myEvent); Serial.print(" in state: "); Serial.println((char)myState);
            break;
        //fan goes brrrr
        }
        break;

      case dehum_State:
        switch(myEvent) {
          case noneEvent:
            //Serial.println("fan mode, no event");
            break;

          case Floater: //fall thru
          case Overheat: //todo: proper handling would just mean stopping the pump until no longer over max, then resuming when under max, for now bomb out as I don't expect to hit this limit
          case StopDehum:
            Serial.println("dehum_State: going to pumpCool_State");
            stopPump();
            
            //coolStartTime = millis();
            //Serial.print("Dehum State,  event, cooltime: "); Serial.println(cooltime);
            nextState = pumpCool_State;
            goto stopper; //early exit

          case StartDehum:
            startDehum();
            break;

           default:
            Serial.print("Unhandled event: "); Serial.print((char)myEvent); Serial.print(" in state: "); Serial.println((char)myState);
            break;
        }

        //no new events to process, pump goes brrrrr?

        setFan(1);

        calcTargetTemp();

        if(tubeTargetTemp < tubeTemp)
          startPump();
        else stopPump();

        break;

      case pumpCool_State:
        switch(myEvent) {
          case noneEvent:
            //Serial.println("pump cool state, no event");
            break;
            
          case SetFan:
            setFan(newfan);
            nextState = fan_State;
            Serial.println("Pump Cool State, Start Fan");
            break;
  
          case StartDehum:
            startDehum();
            break;
          default:
            Serial.print("Unhandled event4: "); Serial.print((char)myEvent); Serial.print(" in state: "); Serial.println((char)myState);
             break;
        }

        if (millis() - pumpStopTime > coolDownTime ) {
          setFan(0);
          Serial.println("pumpCool State: going idle");
          nextState = idle_State;
        }
        else {}
        break;

      default:
        Serial.println("unhandled state");
        break;
    }

    stopper:

    myEvent = noneEvent;

    screenPostLoop();
  }

} *myFirstDehum; //class Dehumidifier

//////////////////////////////////////// SETUP CODE
void led_setup() {
  Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Enable*/, true /*Serial Enable*/);
  Heltec.display->clear();
  Heltec.display->drawString(0,0, espName);
  Heltec.display->display();
}

void wifi_setup() {
  Heltec.display->drawString(0,10,"wifi..");
  Heltec.display->display();
  char i =0;
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  WiFi.config(staticIP, gateway, gateway, subnet);

  while ((WiFi.status() != WL_CONNECTED) & (i < 35)) {
    i++;
    delay(100);
  }

  sprintf(line, "i: %i", i);
  Heltec.display->drawString(40, 10, line);

  if(WiFi.status() != WL_CONNECTED)
    Heltec.display->drawString(16, 10, "...Ain't no wifi!"); 
  else
    Heltec.display->drawString(16,10,"...done");

  Heltec.display->display();
}

void ota_setup() {
  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });

  ArduinoOTA.begin();
}

void mqtt_setup() {
  Heltec.display->drawString(60,10,"mqtt..");
  Heltec.display->display();

  mqtt.setServer(mqtt_server, 1883);

  mqtt.setCallback(callback);

  mqtt_connect();

  mqtt.subscribe("canopy/temperature");
  mqtt.subscribe("canopy/humidity");
  
  mqtt.subscribe("espDehum/setInterval");
  mqtt.subscribe("espDehum/setCoolDown");
  mqtt.subscribe("espDehum/setCoilTemp");
  mqtt.subscribe("espDehum/setDewCalibration");

  mqtt.subscribe("espDehum/setFan");
  mqtt.subscribe("espDehum/dehum");

//diagnostic commands
#ifdef DIAG
  mqtt.subscribe("espDehum/sfan");
  mqtt.subscribe("espDehum/mfan");
  mqtt.subscribe("espDehum/ffan");
  mqtt.subscribe("espDehum/comp");
  mqtt.subscribe("espDehum/reboot");
#endif

  mqtt.publish("espDehum/alert", "Hello world ");

  Heltec.display->drawString(90,10,"..done");
  Heltec.display->display();
}

void mqtt_connect() {
  while (!mqtt.connected())
    if (!mqtt.connect(espName)) delay(800);
}

void callback(char* topic, byte* payload, unsigned int length) {
//I want to use strcat(espName, "/fan") but something's fucky

  #ifdef DEBUG
    Serial.print("callback topic: ");
    Serial.print(topic);
    Serial.print(" payload: ");
    Serial.println((char *) payload);
    //Serial.print(sizeof( (char *)payload));
  #endif

  if (0 == strcmp(topic, "espDehum/setInterval")) {
      interval = atoi((char *)payload)/10; //no sanity checking here
      //Serial.print("new interval: ");Serial.println(interval);
    }

   else
    myFirstDehum->callback(topic, payload, length);
}

void setup() {

  pinMode(LEDGPIO, OUTPUT);

  Serial.begin(115200);
  Serial.println("\nDehumdifier\n\n");

  led_setup();

  wifi_setup();

  ota_setup();

  mqtt_setup();

  myFirstDehum = new Dehumidifier;

  lastLoop = millis();
}

void loop() {
  digitalWrite(LEDGPIO, HIGH); //LED heartbeat

  if (WiFi.status() != WL_CONNECTED )
      wifi_setup();
      
  ArduinoOTA.handle();
  
  if (!mqtt.connected())
    mqtt_connect();

  mqtt.loop();

  myFirstDehum->loop();

  digitalWrite(LEDGPIO, LOW);

  while(millis()-lastLoop < interval)
    if(!(millis() % 250)) mqtt.loop();

  lastLoop = millis();
}
Post Reply