스마트 홈/esp32

아버지 비닐 하우스 개폐기 외전(하나더 달기) - 진짜 최종

다자바무커 2022. 1. 16. 23:10
반응형
개폐기 설치를 성공적으로 마치면서 나머지 한쪽도 달자고 하셨다.
근데 원래는 양쪽 모두 열리는 정도가 같은 걸로 생각하고 그렇게 만들 생각이었는데 따로 해야한다고 하신다 ㄷㄷ
까라면 까야지 뭐...
처음엔 토픽을 따로 설정해서 따로 읽어 들여 같은 방식으로 간단하게 만들 생각이었는데
ESP32하나로 두 토픽을 동시에 수신하기는 힘들어 보였다.
그래서 결국 하나의 토픽으로 명령을 구분해서 만들어야하는데 결국 상당히 간단?하게 만들기로 했고

 

 

결국! 성공했다.

아버지 하우스에 겨울(1월 중순)인제 목련이 피더라 ㅋ

과정


앞서 말했다 시피 하나의 토픽으로 받은 메세지를 구분하여 작동하도록 해야한다.
원래 방식은 숫자를 입력받아 그 숫자 만큼 움직이도록 했는데 오른쪽 개폐기는 원래 방식대로 숫자와 STOP을 입력 받고
나머지 하나는 open close stop 의 세가지 메세지를 받도록했다.

 

 먼저, 홈어시스턴트(HomeAsistant)에서 먼저 코드를 수정했다.

홈어시스턴트에 접속하고 configuration.yaml에서 먼저 개폐기 코드를 복사한 다음

1. Esp32로 보낼 토픽은 동일함으로 수정하지 않고

2.각 개폐기당 위치(열린 정도)를 입력받는 내용은 다름으로 토픽을 달리한다.

3. 열림 닫힘 멈춤 메세지는 문자로 수정한다.

4. retain을 true로 설정해야 Esp32에서 다시 접속하더라도 같은 메세지를 받는다.

5. 포지션은 열린 정도라 동일하게 100, 0으로 설정한다.

 


이제 아두이노(Esp32)에서 수신토픽(house/right/motor/set)에서 받은 메세지를 숫자인지 문자인지로 구분해서 어떤 모터를 컨트롤해야하는지를 알려주고 그에따라 컨트롤하도록 한다.
중지는 우측 좌측 개폐기 모두 문자로 수신함으로 좌측은 소문자로 'stop' 우측은 대문자로 'STOP'으로 하여 구분한다.
우측은 원래대로 숫자로 수신함으로 한번에 몇 %를 열고 싶은지를 전달할 수도 있지만
좌측은 중지 열림 닫힘만 전달 받음으로 한번에 얼마나 열지 구분하는데 어려움이 있다.

 

아두이노(Esp32)코드  톺아보기

//하우스 개폐기

#include <WiFi.h>
#include <PubSubClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <EEPROM.h>

#define ONE_WIRE_BUS 4
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

const char *ssid =  "KT_WLAN_A914";   
const char *password =  "000000214F"; 

const char* ID = "걔폐기";  // Name of our device, must be unique
const char* TOPIC = "house/right/motor/pos";
const char* TOPICLEFT = "house/right/motor/posleft";
const char* INTOPIC = "house/right/motor/set";
const char* TEMPTOPIC = "house/temperature";
const char* mqttUser = "dragon";
const char* mqttPassword = "qazwsxedcrfvtgbyhnujm";
const char* broker = "홈어시스턴트 외부 아이피";

WiFiClient wclient;
PubSubClient client(wclient); 
char messages[50];
char messagesl[50];
char tempers[50];
int SW_state = 0;
int Motor_open1 = 12;
int Motor_close1 = 13;
int Motor_open2 = 26;
int Motor_close2 = 27;
unsigned long setTime = 0;
unsigned long preTime = 0;
float tem = 0;
float tem1 = 0;
char val[20];
unsigned long time1 = 0;
unsigned long time2 = 0;
unsigned long time3 = 0;
unsigned long time4 = 0;
unsigned long timeState = 0;
unsigned long rollover = 0;
unsigned long rolloverl = 0;
String msg = "";
String msgFormer = "";
String msgRight = "l";
String msgLeft = "";
String msgLeftState = "";
int maxTime = 600; 

// Connect to WiFi network
void setup_wifi() {
  Serial.print("\nConnecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password); 
  while (WiFi.status() != WL_CONNECTED) { 
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void sendMessage() {
  if (setTime > preTime){
    time1 = setTime - preTime;
    preTime += time1;
  } else {
    time1 = preTime - setTime;
    preTime -= time1;
  }
  Serial.println(time1);
  snprintf(messages, 75, "%ld", preTime/maxTime);
  client.publish(TOPIC, messages);
  msgRight = "dont";
  EEPROM.write(1, preTime/maxTime);
  EEPROM.commit();
}

bool isNumber(const String& str)
{
    for (char const &c : str) {
        if (std::isdigit(c) == 0) return false;
    }
    return true;
}


void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Received messages: ");
  Serial.println(INTOPIC);
  msg = "";
  for(int i=0; i<length; i++){
    //Serial.println((char)payload[i]);
    msg += (char)payload[i];
  }
  Serial.println(msg);
  if (msg == msgFormer) {
  } else if (msg == "STOP") {
    msgRight = "STOP";
    msgFormer = msg;
  } else {
    if (isNumber(msg)){
      msgRight = msg;
      rollover = millis();
      Serial.println("is number");
    } else {
      msgLeft = msg;
      rolloverl = millis();
    }
    msgFormer = msg;
  }
}


// Reconnect to client
void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(ID, mqttUser, mqttPassword)) {
      Serial.println("connected");
      Serial.print("Publishing to: ");
      Serial.println(TOPIC);
      Serial.println('\n');
      client.subscribe(INTOPIC);

    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println("\n try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200); 
  pinMode(4, INPUT);
  pinMode(Motor_open1, OUTPUT);
  pinMode(Motor_close1, OUTPUT);
  pinMode(Motor_open2, OUTPUT);
  pinMode(Motor_close2, OUTPUT);  
  delay(100);
  setup_wifi(); // Connect to network
  client.setServer(broker, 1883);
  client.setCallback(callback);
  sensors.begin();
  EEPROM.begin(8);
  preTime = EEPROM.read(1) * maxTime;
  timeState = EEPROM.read(0) * maxTime;
}

void loop() {
  if (!client.connected()){
    reconnect();
  }
  client.loop();
  sensors.requestTemperatures();
  tem = sensors.getTempCByIndex(0);
  tem = floor(tem*10)/10;
  //Serial.print("temp=");
  //Serial.println(tem);
  if (tem < -55){
    
  } else if (tem != tem1) {
    tem1 = tem;
    dtostrf(tem, 5, 1, val);
    snprintf(tempers, 75, "%s", val);
    //Serial.print("tempers=");
    //Serial.println(tem);
    client.publish(TEMPTOPIC, tempers);
  }
  if (msgLeft == "open"){
    Serial.println("열림");
    digitalWrite(Motor_open1, HIGH);
    time4 = millis();
    time3 = time4 - rolloverl;
    msgLeftState = "open";
    if (time3 >= maxTime*100 - timeState){ 
      digitalWrite(Motor_open1, LOW);
      msgLeft = "dont";
      timeState = maxTime*100;
      EEPROM.write(0, timeState/maxTime);
      EEPROM.commit();
      snprintf(messagesl, 75, "%ld", timeState/maxTime);
      client.publish(TOPICLEFT, messagesl);
      Serial.println("goodopen");
    }
  } else if (msgLeft == "close") {
    Serial.println("닫힘");
    digitalWrite(Motor_close1, HIGH);
    time4 = millis();
    time3 = time4 - rolloverl;
    msgLeftState = "close";
    if (time3 >= timeState){ 
      digitalWrite(Motor_close1, LOW);
      msgLeft = "dont";
      timeState = 0;
      EEPROM.write(0, timeState/maxTime);
      EEPROM.commit();
      snprintf(messagesl, 75, "%ld", timeState/maxTime);
      client.publish(TOPICLEFT, messagesl);
      Serial.println("goodclose");
    }
  } else if (msgLeft == "stop"){
    Serial.println("좌 닫힘");
    digitalWrite(Motor_open1, LOW);
    digitalWrite(Motor_close1, LOW);
    if (msgLeftState == "open") {
      timeState += time3;
    } else if(msgLeftState == "close") {
      timeState -= time3;
    }
    msgLeftState == "";
    snprintf(messagesl, 75, "%ld", timeState/maxTime);
    client.publish(TOPICLEFT, messagesl);
    Serial.println(messagesl);
    msgLeft = "dont";
    EEPROM.write(0, timeState/maxTime);
    EEPROM.commit();
  }
  if (isNumber(msgRight)) {
    Serial.println("tiktok");
    setTime = maxTime * msgRight.toInt();
    time2 = millis();
    time1 = time2 - rollover;
    if (setTime > preTime) {
      digitalWrite(Motor_open2, HIGH);
      if (time1 >= (setTime - preTime)){
        Serial.println(time1);
        digitalWrite(Motor_open2, LOW);
        sendMessage();
      }
    } else if (setTime < preTime) {
      digitalWrite(Motor_close2, HIGH);
      if (time1 >= (preTime - setTime)){
        Serial.println(time1);
        digitalWrite(Motor_close2, LOW);
        sendMessage();
      }
    } else {
      msgRight = "dont";
    }
  } else if (msgRight == "STOP"){
    Serial.println("우 멈춤");
    digitalWrite(Motor_open2, LOW);
    digitalWrite(Motor_close2, LOW);
    if (setTime > preTime) {
      preTime += time1;
    } else {
      preTime -= time1;
    }
    Serial.println(time1);
    snprintf(messages, 75, "%ld", preTime/maxTime);
    client.publish(TOPIC, messages);
    msgRight = "dont";
    EEPROM.write(1, preTime/maxTime);
    EEPROM.commit();
  }
}

 

/// 바뀐 부분만 살펴보도록하자 ///

#include <EEPROM.h>

먼저 이번에는 정전도 고려하여 정전 후 다시 부팅되더라도 이전에 얼마나 열렸는지 저장하기 위해 아두이노의 메모리를 일부 할당하는 헤더를 가지고 왔다.

 

*변수는 제외하고 본다.

 

void sendMessage() {
  if (setTime > preTime){
    time1 = setTime - preTime;
    preTime += time1;
  } else {
    time1 = preTime - setTime;
    preTime -= time1;
  }
  Serial.println(time1);
  snprintf(messages, 75, "%ld", preTime/maxTime);
  client.publish(TOPIC, messages);
  msgRight = "dont";
  EEPROM.write(1, preTime/maxTime);
  EEPROM.commit();
}

홈어시스턴트로 메세지를 전달하는 함수에서 음수는 -=로는 잘 변경되지 않아

조정한 값이 이전에 열린정도보다 작을 경우는 빼도록 새로 구분하여 설정했고

처음에 0~100%의 값을 받으면 모터 가동시간을 상황에 맞게 조정하기 위해 0~100%의 값을 받으면 maxTime을 곱하게 되는데 다시 홈어시스턴트로 보낼 때에는 나누어 주어야 다시 0~100%로 값이 전달 됨으로 나누어 전달하고

EEPROM의 경우 주소마다 1바이트르 넣을 수 있음으로 조정된 열린 정도의 값을 저장하도록했다.

 

bool isNumber(const String& str)
{
    for (char const &c : str) {
        if (std::isdigit(c) == 0) return false;
    }
    return true;
}

전달 받은 메세지가 문자인지 숫자인지 구분하는 함수이다.

 

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Received messages: ");
  Serial.println(INTOPIC);
  msg = "";
  for(int i=0; i<length; i++){
    //Serial.println((char)payload[i]);
    msg += (char)payload[i];
  }
  Serial.println(msg);
  if (msg == msgFormer) {
  } else if (msg == "STOP") {
    msgRight = "STOP";
    msgFormer = msg;
  } else {
    if (isNumber(msg)){
      msgRight = msg;
      rollover = millis();
      Serial.println("is number");
    } else {
      msgLeft = msg;
      rolloverl = millis();
    }
    msgFormer = msg;
  }
}

메세지 수신 함수 중에 달라진 부분은

  Serial.println(msg);
  if (msg == msgFormer) {
  } else if (msg == "STOP") {
    msgRight = "STOP";
    msgFormer = msg;
  } else {
    if (isNumber(msg)){
      msgRight = msg;
      rollover = millis();
      Serial.println("is number");
    } else {
      msgLeft = msg;
      rolloverl = millis();
    }
    msgFormer = msg;
  }
}

전달 받은 msg가 바로 전에 받은 메세지(msgFormer)랑 동일하면 이상하게 작동하는 경우가 생겨 바로 전에 받은 메세지랑 같으면 무시하도록 했다.

msgRight는 우측 개폐기 메세지로 msgLeft는 좌측 개폐기 메세지로 구분했다.

숫자이거나 "STOP"이면 우측 계패기 메세지 msgRight 변수에 저장하고 msgRight의 메세지를 받은 시각을 rollover에 저장한다.

문자인데 "STOP"을 제외한 메세지인 "open", "closr", "stop"은 좌측 계패기 메세지 msgLeft 변수에 저장하고 msgLeft의 메세지를 받은 시각을 rolloverl에 저장한다.

메세지를 이전 메세지 변수(msgFormer)에 저장한다.

 

void setup() {
  Serial.begin(115200); 
  pinMode(4, INPUT);
  pinMode(Motor_open1, OUTPUT);
  pinMode(Motor_close1, OUTPUT);
  pinMode(Motor_open2, OUTPUT);
  pinMode(Motor_close2, OUTPUT);  
  delay(100);
  setup_wifi(); // Connect to network
  client.setServer(broker, 1883);
  client.setCallback(callback);
  sensors.begin();
  EEPROM.begin(8);
  preTime = EEPROM.read(1) * maxTime;
  timeState = EEPROM.read(0) * maxTime;
}

//달라진 부분
EEPROM.begin(8);
  preTime = EEPROM.read(1) * maxTime;
  timeState = EEPROM.read(0) * maxTime;

메모리 사용을 선언하고 메모리에 저장된 열린 정도를 각각 받아온다.

preTime: 우측 개폐기, 

timeState: 좌측 개폐기

 

루프는 메세지 별로 구분해서 살펴본다.

좌측

if (msgLeft == "open"){
    Serial.println("열림");
    digitalWrite(Motor_open1, HIGH);
    time4 = millis();
    time3 = time4 - rolloverl;
    msgLeftState = "open";
    if (time3 >= maxTime*100 - timeState){ 
      digitalWrite(Motor_open1, LOW);
      msgLeft = "dont";
      timeState = maxTime*100;
      EEPROM.write(0, timeState/maxTime);
      EEPROM.commit();
      snprintf(messagesl, 75, "%ld", timeState/maxTime);
      client.publish(TOPICLEFT, messagesl);
      Serial.println("goodopen");
    }
  } else if (msgLeft == "close") {
    Serial.println("닫힘");
    digitalWrite(Motor_close1, HIGH);
    time4 = millis();
    time3 = time4 - rolloverl;
    msgLeftState = "close";
    if (time3 >= timeState){ 
      digitalWrite(Motor_close1, LOW);
      msgLeft = "dont";
      timeState = 0;
      EEPROM.write(0, timeState/maxTime);
      EEPROM.commit();
      snprintf(messagesl, 75, "%ld", timeState/maxTime);
      client.publish(TOPICLEFT, messagesl);
      Serial.println("goodclose");
    }
  } else if (msgLeft == "stop"){
    Serial.println("좌 닫힘");
    digitalWrite(Motor_open1, LOW);
    digitalWrite(Motor_close1, LOW);
    if (msgLeftState == "open") {
      timeState += time3;
    } else if(msgLeftState == "close") {
      timeState -= time3;
    }
    msgLeftState == "";
    snprintf(messagesl, 75, "%ld", timeState/maxTime);
    client.publish(TOPICLEFT, messagesl);
    Serial.println(messagesl);
    msgLeft = "dont";
    EEPROM.write(0, timeState/maxTime);
    EEPROM.commit();
  }

callback 함수에서 받은 msgLeft의 메세지가 open이거나 close이면 좌측 개폐기 메세지를 수신한 시간(rolloverl)과 현재 시간(time4)의 차를 time3(시간 간격)에 저장하여 루프를 돌면서 시간 간격이 일정이상이 되면 멈추도록한다.

"open"의 경우 전체 열린 정도에서 이전에 열린정도의 차(남은 정도) 이상이 되어 다 열린 상태가 되면 멈추고 메세지를 "dont"로 변경하여 더이상 이 코드는 실행되지 않도록하고 EEPROM(0)에 다 열렸다고 열린 정도를 저장하고 홈어시스턴트에 알려준다.

 "close"의 경우 열린 정도가 다시 닫힘으로 되어야하니 열린 만큼 모털르 반대로 돌려 다 닫히면 메세지를 마찬가지로 "dont"로 하여 더이상 이 코드는 실행하지 않고 EEPROM(0)에 열린 정도0%를 저장하고 마찬가지로 홈어시스턴트에 알려준다.

"stop"의 경우 즉시 모터를 멈추고 열린 정도를 계산하여 메모리(EEPROM(0))에 저장하고 홈어시스턴트에 알려준다.

 

우측

if (isNumber(msgRight)) {
    Serial.println("tiktok");
    setTime = maxTime * msgRight.toInt();
    time2 = millis();
    time1 = time2 - rollover;
    if (setTime > preTime) {
      digitalWrite(Motor_open2, HIGH);
      if (time1 >= (setTime - preTime)){
        Serial.println(time1);
        digitalWrite(Motor_open2, LOW);
        sendMessage();
      }
    } else if (setTime < preTime) {
      digitalWrite(Motor_close2, HIGH);
      if (time1 >= (preTime - setTime)){
        Serial.println(time1);
        digitalWrite(Motor_close2, LOW);
        sendMessage();
      }
    } else {
      msgRight = "dont";
    }
  } else if (msgRight == "STOP"){
    Serial.println("우 멈춤");
    digitalWrite(Motor_open2, LOW);
    digitalWrite(Motor_close2, LOW);
    if (setTime > preTime) {
      preTime += time1;
    } else {
      preTime -= time1;
    }
    Serial.println(time1);
    snprintf(messages, 75, "%ld", preTime/maxTime);
    client.publish(TOPIC, messages);
    msgRight = "dont";
    EEPROM.write(1, preTime/maxTime);
    EEPROM.commit();
  }

우측 개폐기 메세지가 숫자이거나 "STOP"이면 이전처럼 진행하고 대신 메모리(EEPROM(1))에 열린 정도를 저장하고 홈어시스턴트에 알려준다.

 


고난 


사실 이전에 함수의 우선 순위 때문에 하우스의 온도는 받지 못했었다.

달라스 센서는 -55이하는 측정이 안되지만

때때로 -127도가 찍혀서(꼭 -127만 찍힘) -55 보다 아래일 경우는 무시하도록 했다

 

원래는 물리 스위치도 달라고 하셔서 그 코드(똑딱이 스위치로 우측인지 좌측인지 구별한 뒤 버튼 3개로 메세지를 바꾸어 조절)도 추가하고 납땜까지 진행하였으나

버튼이 눌리지도 않았는데 눌리는 경우가 너무 많이 생겨서 삭제 하기도 했고

아버지가 원하는 것은 완전히 물리적인(비상의 경우(고장))을 원하셔서 새로 고안했다.

파란 박스(릴레이로 그린 것)은 3축 스위치로 할거다.

 

사실 아무리 코드를 살펴봐도 문제가 없는데 우측 모터만 돌아가길래 물리적 손상이 있나 살펴봐도 문제가 없어 고민을 계속하고 자료도 계속 찾아보고 그리고 정말 이지 여러번 확인을 하고도 고장이 믿기지 않아 여러 테스트를 해봐도 L298N 모터드라이브의 고장 증거가 많이 나와 교체해서 납땜까지 새로 하니 이젠 두 모터 보두 동시에 잘 작동한다 ㄷㄷ.

 

 

 

이제 진짜 진짜 리얼 정말 끝이다 만세!!!!

반응형