바른 생각 바른 글

IoT peristaltic pump code operating based on MQTT

지구빵집 2021. 4. 26. 10:31
반응형

 

IoT peristaltic pump code operating based on MQTT 

 

사람 사이에서 관계가 중요하듯 프로그램은 잘 돌아가는 코드가 중요하다. 코드가 잘 돌아가기 위해선 구문, 표현, 규칙, 외관에 얽매이지 않아야 한다. 지켜야 하는 규정만 정확히 지킨다면 남는 것은 자유다. 잘 돌아가는 코드를 굳이 리팩토링하지 않는다. 간결하고 우아한 코드는 허구에 가깝다. 거기다가 유연하고, 강력하고, 변화에 잘 적응하는 코드는 더 큰 거짓말이다. 코드는 그 자체로 모든 걸 설명한다. 모든 코드는 프로글래머의 성격과 그가 가진 사상을 말해준다.

 

젊은 프로글래머 두 분이 주말에 있을 시연회 준비로 연구실에 왔다. 남자는 아두이노 계열의 마이크로 컨트롤러 프로그램을 개발하고 에코 회사 개발자는 자바기반 웹 서버 프로그램을 개발한다. 3번 째 만나는 데 그때마다 송수신 데이터를 협의하고 코드를 조금씩 테스트했다. 복잡한 것들은 과감하게 제거하고 단순한 기능에 집중하기로 했다. 오히려 펌프가 수행할 기능이나 데이터 양은 상대적으로 줄어들고 서버에서 할 일은 많아졌다. 젊은 개발자의 도움을 받아 모든 것이 끝났다.

 

어쩜 그렇게 멋있는지. 내가 프로그래머의 길을 걷는 일이 자랑스럽게 느껴졌다. 

 

일단 눈 여겨 볼 부분, 아니 반드시 꼭 집어넣어야 하는 코드를 아래에 설명하고 전체 잘 작동하는 코드를 마지막에 첨부한다.

 

개발하면서 문제가 된 코드 몇 개를 살펴보자. 일단 

 

MQTT client.publish( topic, Message) 구문을 동작하기가 조금은 어렵다. 그러니꺼 PubSubClient 라이브러리 예제에 보면 송신 코드는 아래와 같이 나와있다. 예제를 참고하기 위해 링크를 걸어둔다. 

PubSubClient sample for ESP8266 Arduino  

 

client.publish(topic, (char*) payload.c_str())

 

여하튼 위 예제에 나오는 방식이 제대로 동작이 안되면 다음과 같이 실행한다. 위와 같은 문제에 대한 토론타래를 링크로 연결한다.

char* toCharArray(string str) {
  return &str[0];
}

void publish(string doc, string topic) {
  client.publish(toCharArray(topic), toCharArray(buffer));
}

 

위 함수를 호출하여 사용하는 문장은 아래에 있으니 참고한다. 

 

client.publish("complete", toCharArray(pubstring));

 

서버와 연결이 끊기면 다시 연결하는 문장을 추가한다. 서버를 구독중이라도 시간이 조금 길어지면 자동으로 구독이 중지되고 통신이 끊기는 상황이 발생한다. 이때 아래 문장으로 현재 연결이 되었다면 데이터를 보내고, 연결이 끊겼다면 다시 연결을 하고 구독중인 토픽을 다시 설정한다.

 

if(!client.connected()) 
{
    client.connect(""); // 앞서 설정한 클라이언트 ID로 연결합니다.
    client.subscribe("operate"); // inTopic 토픽을 듣습니다. topic 츨 구독
}    

if (client.connected()) 
{
    client.publish("complete", toCharArray(pubstring));
    Serial.print("complete");
}

 

단순 if 문이 두 개다. 남자는 코드의 라인 수를 늘리기 위해 BSD 구문을 좋아한다. 무엇보다 코드를 변경하기 쉽고, 읽기가 쉽다.

 

참고자료

위키백과 MQTT

코딩스타일

 

 

잘 돌아가는 전체코드인데 보기에는 발로 짠 것 같지만 실제 발로 짠 코드다. ^^

 

#include <SPI.h>
#include <WiFiNINA.h> // NANO 33 IoT에서 Wi-Fi 기능을 사용하기 위한 라이브러리 입니다.
#include <PubSubClient.h>
#include <SimpleDHT.h>
#include "arduino_secrets.h"

//please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID;        // your network SSID (name)
char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)

//24V on off Relay
int relay_control = 2;
//define DHT11 pin connection
int pinDHT11 = 9;
SimpleDHT11 dht11;
//define RGB LED
int red = 4;
int green = 5;
int blue = 6;

//define timer sampling rate 1000=1sec
uint32_t sampleRate = 1000; //sample rate in milliseconds, determines how often TC5_Handler is called
//time variable
unsigned char flag_5seconds = 0; 
uint32_t time_tick_onesecond=0;
uint32_t time_counter_30sec = 0; //second infinit counter
uint32_t ultrasonic_count = 0; //second infinit counter
uint32_t every_5sec_counter = 0; //every 5second
uint32_t flag_300seconds = 0;

const char* mqtt_server = "192.168.168.107"; // MQTT 서버를 입력합니다.
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;

void setup_wifi() {
  delay(10);
  WiFi.begin(ssid, pass); // 앞서 설정한 ssid와 페스워드로 Wi-Fi에 연결합니다.
  while (WiFi.status() != WL_CONNECTED) { // 연결될 때 까지 0.5초 마다 Wi-Fi 연결상태를 확인합니다.
    delay(500);
  }  
  randomSeed(micros()); // 렌덤 문자를 위한 렌덤 시드를 설정합니다.
  
  Serial.println("Connected to WiFi");
  printWifiStatus();
}

int Loop = 0; // LED의 토글을 위해 사용되는 변수입니다.
String ordercode;
String machinecode;
String pubstring;
int pumpout_time;
unsigned char pumpout_flag = 1;


void callback(char* topic, byte* payload, unsigned int length) {
  // Topic에 메시지가 도착하면 실행되는 콜백입니다.
  //Serial.println(length);

  String myString = String((char *)payload);
  String realString = myString.substring(0, length);
  Serial.println(myString.length());
  Serial.println(realString);

  //parsing: 기계코드와 용량 분리
  /// 서버에서 용량을 보낸다. 기계코드, 추출량 ml, 주문번호(추출 완료 후 다시 보낸다)
  int first = realString.indexOf(","); // 첫번째 콤마위치
  int second = realString.indexOf(",",first+1); // 두번째 콤마 위치
  int third = realString.indexOf(",",second+1); // 세번째 콤마 위치
  int strlength = realString.length(); // 문자열 길이
  String str1 = realString.substring(0, first); // 첫번째 토큰
  String str2 = realString.substring(first+1, second); // 두번째 토큰*/
  ordercode = realString.substring(second+1, strlength); // 세번째 토큰*/
  Serial.println(str1);
  Serial.println(str2);
  Serial.println(ordercode);

  pumpout_time = str2.toInt();
  pumpout_flag = 1;

}

void pump_operate(int pump_out_time)
{
  Serial.print(pump_out_time);
  Serial.println(" ml pump on");

  if(pump_out_time == 100)
  {
    digitalWrite(relay_control, LOW);
    //delay(16880);
    delay(3000);
    digitalWrite(relay_control, HIGH);
  }

  if(pump_out_time == 200)
  {
    digitalWrite(relay_control, LOW);
    //delay(16880);
    delay(3000);
    digitalWrite(relay_control, HIGH);
  }
  

  if(pump_out_time == 300)
  {
    digitalWrite(relay_control, LOW);
    //delay(16880);
    delay(18160);
    digitalWrite(relay_control, HIGH);
  }

  if(pump_out_time == 500)
  {
    digitalWrite(relay_control, LOW);
    //delay(26500);
    delay(30180);
    digitalWrite(relay_control, HIGH);
  }  
}

void reconnect() {
  while (!client.connected()) {
//    String clientId = "ArduinoNANO33IoTClinet-"; // 클라이언트 ID를 설정합니다.
//    clientId += String(random(0xffff), HEX); // 같은 이름을 가진 클라이언트가 발생하는것을 방지하기 위해, 렌덤 문자를 클라이언트 ID에 붙입니다.
//    if (client.connect(clientId.c_str())) { // 앞서 설정한 클라이언트 ID로 연결합니다.
    if (client.connect("")) { // 앞서 설정한 클라이언트 ID로 연결합니다.
      client.subscribe("operate"); // inTopic 토픽을 듣습니다. topic 츨 구독
    } else {
      delay(5000);
    }
  }
}

void setup() {
  pinMode(relay_control, OUTPUT);     // 2번 relay control - low enable
  digitalWrite(relay_control, HIGH);
  Serial.begin(9600);
  setup_wifi();

  //setup Timer
  tcConfigure(sampleRate); //configure the timer to run at <sampleRate>Hertz
  tcStartCounter(); //starts the timer
  
  client.setServer(mqtt_server, 1883); // MQTT 서버에 연결합니다.
  client.setCallback(callback);
}

void loop() {
       
    //여기 서버 연결 안되면 계속 실행-타이머 문제 아님
  if (!client.connected()) {
    reconnect(); 
  }  
  client.loop();

  // 5분에 한 번씩 보낸다. test는 10초 만들어 
  //client.publish("log", "kawng, re12345, 25.6, 67.4"); //기계코드(string), 온도(float), 습도(float), 정상유무(bool) ->> kawng, re12345, 25.6, 67.4" - 5분 
  //client.publish("complete", "기계코드, T/F");
  // 두번째는 용량 보내면 동작 후 보낸다. 기계코드, T/F

  
  //String MachineCode = 10001;
  //String pubstring;
  
  delay(500);
  pubstring="";
  pubstring = String(10001);
  pubstring += ",";
  pubstring += String(get_temp());
  pubstring += ",";
  delay(500);  
  pubstring += String(get_humi());
  pubstring += ",";
  pubstring += "T";

  //flag를 두어야지   
  if(flag_300seconds > 300)
  {
    flag_300seconds = 0;    
    client.publish("log", toCharArray(pubstring));
    
    delay(200);
    Serial.print(get_temp()); Serial.print(" *C, ");
    delay(200);
    Serial.print(get_humi()); Serial.println(" H");
  }

  if(pumpout_flag == 1)
  {
    Serial.println(pumpout_time);
    pumpout_flag = 0;
      
    // 시간만큼 동작한다.
    pump_operate(pumpout_time);

    pubstring="";
    pubstring = String(10001);
    pubstring += ",";
    pubstring += ordercode;
    pubstring += ",";
    pubstring += "T";;
    Serial.print(pubstring);
    //client.publish("complete", toCharArray(pubstring));
    if(!client.connected()) {
      client.connect(""); // 앞서 설정한 클라이언트 ID로 연결합니다.
      client.subscribe("operate"); // inTopic 토픽을 듣습니다. topic 츨 구독
    }
    if (client.connected()) {
      client.publish("complete", toCharArray(pubstring));
      Serial.print("complete");
    }
  }
}

char* toCharArray(String str) {
  return &str[0];
}

//void publish(string doc, string topic) {
//  client.publish(toCharArray(topic), toCharArray(buffer));
//}

void printWifiStatus() {
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your board's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

//온도 습도 데이터를 수집하여 5분에 한번 보낸다. 데이터는 int
int get_temp()
{
  //delay(2000);
  byte temperature = 0;
  byte humidity = 0;
  
  if (dht11.read(pinDHT11, &temperature, &humidity, NULL)) {
    Serial.print("Read DHT11 temp failed.");
    return 0;
  }
  else{
    return (int)temperature;  
  }
}

int get_humi()
{
  //delay(2000);
  byte temperature = 0;
  byte humidity = 0;
  
  if (dht11.read(pinDHT11, &temperature, &humidity, NULL)) {
    Serial.print("Read DHT11 humi failed.");
    return 0;
  }else{
    return (int)humidity;
  }
}

void test_topic_split()
{
  String readTopic="abcdefghijklmn,300";
  int length = readTopic.length();
  Serial.println(length);
  String readTopicLength = readTopic.substring(0, length);
 
  //parsing: 기계코드와 용량 분리
  /// 서버에서 용량을 보낸다. 기계코드, 추출량 단위는 ml
  int first = readTopicLength.indexOf(","); // 첫번째 콤마위치
  int second = readTopicLength.indexOf(",",first+1); // 두번째 콤마 위치
  int strlength = readTopicLength.length(); // 문자열 길이
  String str1 = readTopicLength.substring(0, first); // 첫번째 토큰
  String str2 = readTopicLength.substring(first+1, second); // 두번째 토큰
  Serial.println(str1);
  Serial.println(str2);

  int operationTime = str2.toInt();
  
}


//Here Start Timer function
//this function gets called by the interrupt at <sampleRate>Hertz

void TC5_Handler (void) {
  //YOUR CODE HERE  
  time_tick_onesecond++;
  if((time_tick_onesecond%5) == 0) flag_5seconds = 1;
  
  flag_300seconds++;
  //every_5sec_counter++;
  //Serial.println(time_tick_onesecond);
  
  /*if(state == true) {
    digitalWrite(LED_PIN,HIGH);
  } else {
    digitalWrite(LED_PIN,LOW);
  }
  state = !state;*/
  // END OF YOUR CODE
  TC5->COUNT16.INTFLAG.bit.MC0 = 1; //Writing a 1 to INTFLAG.bit.MC0 clears the interrupt so that it will run again
}

/* 
 *  TIMER SPECIFIC FUNCTIONS FOLLOW
 *  you shouldn't change these unless you know what you're doing
 */

//Configures the TC to generate output events at the sample frequency.
//Configures the TC in Frequency Generation mode, with an event output once
//each time the audio sample frequency period expires.
 void tcConfigure(int sampleRate)
{
 // select the generic clock generator used as source to the generic clock multiplexer
 GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID(GCM_TC4_TC5)) ;
 while (GCLK->STATUS.bit.SYNCBUSY);

 tcReset(); //reset TC5

 // Set Timer counter 5 Mode to 16 bits, it will become a 16bit counter ('mode1' in the datasheet)
 TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;
 // Set TC5 waveform generation mode to 'match frequency'
 TC5->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;
 //set prescaler
 //the clock normally counts at the GCLK_TC frequency, but we can set it to divide that frequency to slow it down
 //you can use different prescaler divisons here like TC_CTRLA_PRESCALER_DIV1 to get a different range
 TC5->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIV1024 | TC_CTRLA_ENABLE; //it will divide GCLK_TC frequency by 1024
 //set the compare-capture register. 
 //The counter will count up to this value (it's a 16bit counter so we use uint16_t)
 //this is how we fine-tune the frequency, make it count to a lower or higher value
 //system clock should be 1MHz (8MHz/8) at Reset by default
 TC5->COUNT16.CC[0].reg = (uint16_t) (SystemCoreClock / sampleRate);
 while (tcIsSyncing());
 
 // Configure interrupt request
 NVIC_DisableIRQ(TC5_IRQn);
 NVIC_ClearPendingIRQ(TC5_IRQn);
 NVIC_SetPriority(TC5_IRQn, 0);
 NVIC_EnableIRQ(TC5_IRQn);

 // Enable the TC5 interrupt request
 TC5->COUNT16.INTENSET.bit.MC0 = 1;
 while (tcIsSyncing()); //wait until TC5 is done syncing 
} 

//Function that is used to check if TC5 is done syncing
//returns true when it is done syncing
bool tcIsSyncing()
{
  return TC5->COUNT16.STATUS.reg & TC_STATUS_SYNCBUSY;
}

//This function enables TC5 and waits for it to be ready
void tcStartCounter()
{
  TC5->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE; //set the CTRLA register
  while (tcIsSyncing()); //wait until snyc'd
}

//Reset TC5 
void tcReset()
{
  TC5->COUNT16.CTRLA.reg = TC_CTRLA_SWRST;
  while (tcIsSyncing());
  while (TC5->COUNT16.CTRLA.bit.SWRST);
}

//disable TC5
void tcDisable()
{
  TC5->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE;
  while (tcIsSyncing());
}
// end of code.

 

 

IoT로 작동하는 연동펌프

 

 

 

 

 

반응형