This commit is contained in:
choibk 2025-11-08 11:32:34 +09:00
parent 44e486d73f
commit 319eb23eba
4 changed files with 2047 additions and 11 deletions

112
README Normal file
View File

@ -0,0 +1,112 @@
[2025-09-20] ESP32_R3.1 / MPINO_R1.4
ESP32 I²C↔MQTT Bridge & MPINO Control — README
이 문서는 현 스케치 기준으로 사용하는
시리얼 명령어(Serial CLI) 와 MQTT 토픽/명령(페이로드 형식) 을 한눈에 정리한 것입니다.
베이스 토픽, Wi-Fi·MQTT 설정은 시리얼 CLI로 변경할 수 있으며 NVS에 영구 저장됩니다.
________________________________________
개요
• ESP32가 I²C 마스터로 MPINO 슬레이브를 폴링하여 센서/설정 프레임을 수신하고 MQTT로 퍼블리시합니다.
• 외부에서 MQTT 명령을 보내면 ESP32가 I²C로 3바이트 명령을 슬레이브에 전달합니다.
• 설정은 모두 Preferences(NVS) 에 저장되어 전원 재인가 후에도 유지됩니다.
• 저장된 Wi-Fi AP가 1개도 없으면 부팅 시 자동으로 CONFIG 모드에 진입합니다.
________________________________________
시리얼 명령어 (Serial CLI)
명령 형식 설명
HELP HELP 가능한 모든 명령과 사용법을 표시
CONFIG CONFIG 설정 모드 진입 (Wi-Fi/MQTT 연결·I²C 폴링 일시 중지)
EXIT EXIT 또는 CONFIG OFF 설정 모드 종료, 정상 동작 재개
LIST LIST 저장된 Wi-Fi AP 후보 목록 표시(최대 6개)
ADD ADD ssid,password AP 후보 추가
EDIT EDIT index ssid,password AP 후보 수정
DELETE DELETE index AP 후보 삭제
CLEAR CLEAR AP 후보 전체 삭제
MQTTSET MQTTSET server,user,pass,topic MQTT 설정 저장• server는 host 또는 host:port 형식• topic은 베이스 토픽
MQTTINFO MQTTINFO 현재 MQTT 설정과 파생 토픽(status/config/command/…) 표시
REQINFO REQINFO REQID와 REQTIME 현재값 요약 표시
REQID REQID [start,end] 폴링 ID 범위 설정 또는 인자 없이 조회
REQTIME REQTIME [ms] 폴링 주기(ms) 설정 또는 인자 없이 조회 (하한 200 ms)
메모
• 모든 설정은 NVS에 영구 저장됩니다.
• REQTIME 간격마다 I²C를 읽고 유효 프레임이면 바로 MQTT 발행합니다.
• 슬레이브가 SENSOR/CONFIG 프레임을 번갈아 보낸다면, 특정 단일 토픽만 보면 체감 주기가 2 × REQTIME처럼 보일 수 있습니다.
예시
CONFIG
ADD MyAP,myPw
MQTTSET qideun.com,,,"selimcns"
REQTIME 1000
REQID 1,16
EXIT
________________________________________
MQTT 토픽
아래 예시는 베이스 토픽이 selimcns인 경우입니다.
베이스 토픽은 MQTTSET …,<base> 로 변경하면 모든 파생 토픽이 함께 바뀝니다.
퍼블리시(ESP32 → 브로커)
• selimcns/status — 센서값 JSON
• {
• "EC":123, "pH":6.54,
• "airTemperature":23.45, "airHumidity":55.67,
• "ADC0":0, "ADC1":0, "ADC2":0,
• "soilTemp1":21.34, "soilTemp2":22.01,
• "l_minute":0, "pumpPin":0, "valvePin":0,
• "waterLevelPin":0, "rainSensor":0
• }
• selimcns/config — 구성(설정 프레임) JSON
• {
• "currentMode":0,
• "cycleTime":0,
• "cycleRestartDelay":0,
• "valveDelay":0,
• "smStart":0, "smStop":0
• }
• selimcns/bridge — LWT (retain)
연결 시 "online" 발행, 비정상 종료 시 브로커가 "offline" 게시.
• selimcns/speed/response — 핑 응답(ECHO)
구독(브리지 ← 외부)
• selimcns/command — 명령 토픽
o 페이로드 형식: 문자열 "ID,VALUE" (쉼표로 구분된 10진 정수 2개, 공백 없음)
o 동작: ESP32가 I²C로 3바이트 [ID][VALUE LSB][VALUE MSB] 를 슬레이브에 전송
o 권장: retain 사용 금지(재접속 시 재실행 방지)
• selimcns/speed — 핑용 ECHO 입력
수신 페이로드를 그대로 selimcns/speed/response에 재전송
________________________________________
명령 ID 매핑 (슬레이브 receiveData() 기준)
ID 의미 VALUE (단위) 비고
1 모드 전환 0=MANUAL, 1=AUTO, 2=AI 전환 시 stopPumpSequence() 수행
2 펌프 제어 0=OFF, 1=ON AI 모드에서만 유효. 밸브 선개방+지연 후 ON
3 cycleTime 초 펌프 ON 유지 시간 (최대 65,535 s ≈ 18.2 h)
4 cycleRestartDelay 초 펌프 OFF 유지 시간
5 valveDelay 초 밸브 개방 후 펌프 ON까지 딜레이
6 smStart 0~1023 자동 모드 ON 임계 (3개 ADC 중 2개 초과)
7 smStop 0~1023 자동 모드 OFF 임계 (3개 ADC 중 2개 미만)
8 holdDuration 초 조건 유지 시간(채터링 방지)
설정 명령(3~8)을 보내면 슬레이브가 configData를 갱신·CRC 재계산 후 I²C로 내보내며,
브리지가 곧바로 <base>/config에 새 값을 퍼블리시합니다. 이를 간접 ACK 로 활용하세요.
________________________________________
사용 예시
# 자동 모드로 전환
mosquitto_pub -h qideun.com -t selimcns/command -m "1,1"
# 자동 모드 파라미터 설정
mosquitto_pub -h qideun.com -t selimcns/command -m "6,850" # smStart
mosquitto_pub -h qideun.com -t selimcns/command -m "7,600" # smStop
mosquitto_pub -h qideun.com -t selimcns/command -m "5,5" # valveDelay 5s
mosquitto_pub -h qideun.com -t selimcns/command -m "3,300" # cycleTime 5분
mosquitto_pub -h qideun.com -t selimcns/command -m "4,900" # restartDelay 15분
# AI 모드에서 펌프 ON → OFF
mosquitto_pub -h qideun.com -t selimcns/command -m "1,2" # AI 모드
mosquitto_pub -h qideun.com -t selimcns/command -m "2,1" # 펌프 ON
mosquitto_pub -h qideun.com -t selimcns/command -m "2,0" # 펌프 OFF
# 상태/설정 모니터링
mosquitto_sub -h qideun.com -t selimcns/status
mosquitto_sub -h qideun.com -t selimcns/config
________________________________________
참고 & 주의사항
• 베이스 토픽은 MQTTSET의 마지막 인자로 설정합니다. 변경 시 모든 파생 토픽이 자동 갱신됩니다.
• REQTIME은 I²C 폴링 주기입니다. 슬레이브가 SENSOR/CONFIG를 번갈아 보낼 경우, 특정 단일 토픽 기준 체감 주기가 2 × REQTIME처럼 보일 수 있습니다. 필요 시 REQTIME을 절반으로 조정하거나(간단), 코드에서 한 사이클에 2회 폴링하도록 변경할 수 있습니다.
• 명령 토픽은 retain 금지를 권장합니다(재접속 시 동일 명령 재적용 방지).
• 타이밍 명령(3,4,5,8)의 단위는 초, 내부에서는 ms로 변환되어 사용됩니다.
• smStart > smStop(히스테리시스)을 권장합니다.
• 네트워크가 끊기거나 CRC 에러가 나면 해당 주기의 MQTT 발행은 스킵됩니다(로그 확인).

View File

@ -12,3 +12,9 @@
platform = espressif32 platform = espressif32
board = esp32dev board = esp32dev
framework = arduino framework = arduino
lib_deps =
knolleary/PubSubClient@^2.8
bblanchon/ArduinoJson@^7.4.2
paulstoffregen/OneWire@^2.3.8
milesburton/DallasTemperature@^4.0.5
4-20ma/ModbusMaster@^2.0.1

944
src/MPINO.cpp Normal file
View File

@ -0,0 +1,944 @@
// MPINO-16A8R8T Slave Control Board Code R1.1, 2024-11-05
// 수동모드, AI 모드에서 펌프 On 시 밸브가 닫혀있는 상태에서 펌프가 On 되는 현상 수정
#include <Wire.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ModbusMaster.h>
// 송신 주기 및 타이밍 변수 선언
unsigned long lastSendTime = 0;
const unsigned long sendInterval = 50; // 송신 간격 (100ms)
bool sendSensorNext = true; // SensorData 전송 플래그 초기값
#define ONE_WIRE_BUS 12 // DS18B20 온도 센서 핀
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
// 온도 측정값 저장 변수
float soilTemp[2]; // 2개의 온도 값을 저장하는 배열
// RS485 통신 모듈 및 센서 설정
ModbusMaster node1; // ID 1: 토양 센서
ModbusMaster node2; // ID 2: 대기 센서
unsigned long lastRequestTime = 0;
const unsigned long requestInterval = 1000; // 각 센서 데이터 요청 간격 (1초)
// 데이터 변수 선언
int moisture = 0;
float soiltemperature = 0.0;
int ec = 0;
float ph = 0.0;
float airtemperature = 0.0;
float airhumidity = 0.0;
// 타이머 설정
unsigned long previousMillis = 0;
const long interval = 1000; // 데이터 읽기 간격 (1초)
// 핀 정의
const int flowSensorPin = 2; // 유량 센서 핀
const int pumpPin = 64; // 펌프 핀
const int valvePin = 67; // 밸브 핀
const int control_PumpOn = 36;
const int control_PumpOff = 37;
const int waterLevelPin = 31; // 워터 레벨 핀
const int rainSensor = 29; // 강우 센서 핀
const int manualModePin = 33; // 수동 모드 선택 핀
const int autoModePin = 34; // 자동 모드 선택 핀
const int aiModePin = 35; // AI 모드 선택 핀
const int adcPins[3] = {A0, A1, A2}; // 토양 수분 센서 핀들
// 토양 수분 센서 값
int ADC0 = 0;
int ADC1 = 0;
int ADC2 = 0;
// 필터링된 ADC 값
float filtered_ADC[3] = {0, 0, 0};
const float alpha = 0.1; // LPF 강도 설정 (0.1 ~ 0.3 정도 추천)
// 설정 값
int smStart = 850; // 펌프 ON 수분 기준값
int smStop = 600; // 펌프 OFF 수분 기준값
unsigned long valveDelay = 5000; // 밸브 열림 대기 시간 (5초)
unsigned long cycleTime = 60000; // 자동 모드 사이클 시간 (10초)
unsigned long holdDuration = 10000; // 조건 유지 시간 (10초)
// 필요한 변수 추가
unsigned long pumpStartTime = 0;
unsigned long restartDelayStartTime = 0;
unsigned long cycleStartTime = 0; // 초기화 추가, 필요에 따라 사용
unsigned long cycleRestartDelay = 60000;
unsigned long debounceDelay = 200; // 디바운스 딜레이 (1초)
unsigned long lastSensorPrintTime = 0; // 마지막 센서 출력 시간
const unsigned long sensorPrintInterval = 2000; // 센서 출력 간격 (2초)
unsigned long holdStartTime = 0;
bool holdConditionMet = false;
// 유량계 관련 변수
volatile int flow_frequency = 0; // 펄스 카운터
unsigned long cloopTime = 0; // 마지막 유량 측정 시간
float l_minute = 0.0, l_hour = 0.0, l_second = 0.0; // 유량 계산 변수
float minFlow = 0.5; // 초기 값 설정. 필요에 따라 수정 가능
// 상태 변수
bool control_pumpState = false; // 펌프 플래그
bool pumpIsOn = false;
bool valveOpening = false;
bool waterLevelHigh = false;
unsigned long valveOpenStartTime = 0;
unsigned long lastDebounceTime = 0;
bool inCycle = false;
bool autoModeAlerted = false; // 자동 모드에서 연속 메시지 방지
// 전역 변수 선언
bool delayAfterPumpOff = false; // 펌프 종료 후 대기 상태 플래그
unsigned long pumpOffTime = 0; // 펌프가 꺼진 시각 저장
const unsigned long pumpRestartDelay = 5000; // 펌프 재시작 지연 시간 (5초)
int currentStep = 1; // 시퀀스의 시작 단계
// 모드 상태 정의
enum Mode { MANUAL, AUTO, AI };
Mode currentMode = MANUAL;
// 이전 핀 상태 추적 변수
int lastManualPinState = LOW;
int lastAutoPinState = LOW;
int lastAiPinState = LOW;
// 제어 명령 플래그
//bool manualModeRequested = false;
//bool autoModeRequested = false;
bool aiModeRequested = false;
// 모드 전환을 위한 타이머 및 디바운스 변수
unsigned long lastModeChangeTime = 0;
const unsigned long modeChangeDebounceDelay = 1000; // 모드 변경 디바운스 시간 (500ms)
// 버튼을 일정 시간 동안 누를 때만 모드 전환
unsigned long holdTime = 1000; // 모드 전환을 위한 홀드 시간
unsigned long manualHoldStart = 0;
unsigned long autoHoldStart = 0;
unsigned long aiHoldStart = 0;
// 펌프 시퀀스 상태
enum PumpState { IDLE, OPENING_VALVE, PUMP_ON };
PumpState pumpState = IDLE;
//I2C 통신 송수신 설정
const uint8_t SENSOR_DATA_HEADER = 0x01;
const uint8_t CONFIG_DATA_HEADER = 0x02;
#pragma pack(push, 1)
struct SensorData {
uint8_t header = SENSOR_DATA_HEADER;
uint16_t ec;
uint16_t ph;
int16_t airTemperature;
int16_t airHumidity;
uint16_t ADC0;
uint16_t ADC1;
uint16_t ADC2;
int16_t soilTemp1; // 첫 번째 DS18B20 센서 온도
int16_t soilTemp2; // 두 번째 DS18B20 센서 온도
uint16_t l_minute;
uint8_t pumpPin;
uint8_t valvePin;
uint8_t waterLevelPin;
uint8_t rainSensor;
uint16_t crc;
};
#pragma pack(pop)
SensorData sensorData;
// ConfigData 구조체 정의
#pragma pack(push, 1)
struct ConfigData {
uint8_t header = CONFIG_DATA_HEADER; // 1 byte
uint8_t currentMode; // 1 byte (0: Manual, 1: Auto, 2: AI)
uint32_t cycleTime; // 4 bytes
uint32_t cycleRestartDelay; // 4 bytes
uint32_t valveDelay; // 4 bytes
uint32_t smStart; // 4 bytes
uint32_t smStop; // 4 bytes
//uint32_t holdDuration; // 4 bytes
uint16_t crc; // 2 bytes
};
#pragma pack(pop)
ConfigData configData;
uint16_t calculateCRC(uint8_t* data, size_t length) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}
void updateSoilMoistureValues() {
for (int i = 0; i < 3; i++) {
int rawADC = analogRead(adcPins[i]);
// if (i == 2) {
// rawADC += 65; // ADC2에만 65 추가
// }
// LPF 적용
filtered_ADC[i] = alpha * rawADC + (1 - alpha) * filtered_ADC[i];
}
// 필터링된 ADC 값을 ADC0, ADC1, ADC2 변수에 할당
ADC0 = filtered_ADC[0];
ADC1 = filtered_ADC[1];
ADC2 = filtered_ADC[2];
}
// 슬레이브 ID 1의 데이터 읽기 함수
void readSensorDataID1() {
uint8_t result = node1.readHoldingRegisters(0x00, 4);
if (result == node1.ku8MBSuccess) {
moisture = node1.getResponseBuffer(0);
soiltemperature = node1.getResponseBuffer(1) / 10.0;
ec = node1.getResponseBuffer(2);
ph = node1.getResponseBuffer(3) / 10.0;
Serial.println("ID 1 - Soil Sensor Data:");
Serial.print("Moisture: "); Serial.println(moisture);
Serial.print("Temperature: "); Serial.println(soiltemperature, 1);
Serial.print("EC: "); Serial.print(ec); Serial.println(" us/cm");
Serial.println("PH: "); Serial.println(ph, 2);
} else {
Serial.print("Error reading from sensor ID 1. Error code: ");
Serial.println(result);
}
}
// 슬레이브 ID 2의 데이터 읽기 함수
void readSensorDataID2() {
uint8_t result = node2.readInputRegisters(0x0001, 2); // 온도와 습도 데이터 요청
if (result == node2.ku8MBSuccess) {
// Modbus에서 수신된 온도 값
int16_t rawTemperature = node2.getResponseBuffer(0); // 온도 값 (16비트 부호 있는 값)
int16_t rawHumidity = node2.getResponseBuffer(1); // 습도 값 (16비트 부호 없는 값)
// 온도 변환 (Modbus 데이터는 1/10 단위로 제공됨)
airtemperature = rawTemperature / 10.0;
// 습도 변환 (습도는 항상 양수)
airhumidity = rawHumidity / 10.0;
// 디버깅용 출력
Serial.print("ID 2 - Temperature and Humidity Data:");
Serial.print(" Temperature: ");
Serial.print(airtemperature);
Serial.print(" °C, Humidity: ");
Serial.print(airhumidity);
Serial.println(" %");
} else {
Serial.print("Error reading from sensor ID 2. Error code: ");
Serial.println(result);
}
}
// DS18B20 온도 센서 값 읽기
void readDS18B20Data() {
sensors.requestTemperatures(); // 모든 센서로부터 온도 요청
// 첫 번째 센서의 온도 읽기
float temp1 = sensors.getTempCByIndex(0);
soilTemp[0] = (temp1 == DEVICE_DISCONNECTED_C) ? -127.0 : temp1; // 센서 연결 실패 시 -127.0 저장
// 두 번째 센서의 온도 읽기
float temp2 = sensors.getTempCByIndex(1);
soilTemp[1] = (temp2 == DEVICE_DISCONNECTED_C) ? -127.0 : temp2; // 센서 연결 실패 시 -127.0 저장
// 시리얼 출력 (디버깅용)
Serial.print("SoilTemp 1: ");
Serial.print(soilTemp[0]);
Serial.print(" °C, SoilTemp 2: ");
Serial.println(soilTemp[1]);
}
// 유량 센서의 펄스를 처리하는 인터럽트 핸들러
void flow() {
flow_frequency++;
}
// 1초마다 유량을 계산하여 출력하는 함수
void calculateFlowRate() {
unsigned long currentTime = millis();
if (currentTime - cloopTime >= 1000) { // 1초마다 유량 계산
cloopTime = currentTime; // 마지막 계산 시간 갱신
if (flow_frequency == 0) {
l_minute = 0.0;
l_hour = 0.0;
Serial.println("Flow frequency is zero. Setting flow rates to 0.");
} else {
// L/min 및 L/hour 계산
l_minute = (flow_frequency / 7.5); // L/min 단위로 변환 (유량계 사양에 따라 조정 필요)
l_hour = l_minute * 60; // L/hr 계산
// 출력
Serial.print("Flow rate: ");
Serial.print(l_minute);
Serial.print(" L/min, ");
Serial.print(l_hour);
Serial.println(" L/hour");
}
// 펄스 카운터 초기화
flow_frequency = 0;
}
}
void checkMode() {
unsigned long currentMillis = millis();
if (currentMillis - lastModeChangeTime < modeChangeDebounceDelay) {
return;
}
int manualPinState = digitalRead(manualModePin);
int autoPinState = digitalRead(autoModePin);
int aiPinState = digitalRead(aiModePin);
if (manualPinState == HIGH && lastManualPinState == LOW && currentMode != MANUAL) {
currentMode = MANUAL;
Serial.println("Switched to Manual Mode.");
lastModeChangeTime = currentMillis;
} else if (autoPinState == HIGH && lastAutoPinState == LOW && currentMode != AUTO) {
currentMode = AUTO;
Serial.println("Switched to Auto Mode.");
lastModeChangeTime = currentMillis;
// 확인용: 자동 모드 전환 시 cycleTime과 cycleRestartDelay 값 출력
Serial.print("Auto Mode - cycleTime: ");
Serial.println(cycleTime);
Serial.print("Auto Mode - cycleRestartDelay: ");
Serial.println(cycleRestartDelay);
// ConfigData의 값도 출력
Serial.print("ConfigData cycleTime: ");
Serial.println(configData.cycleTime);
Serial.print("ConfigData cycleRestartDelay: ");
Serial.println(configData.cycleRestartDelay);
} else if (aiPinState == HIGH && lastAiPinState == LOW && currentMode != AI) {
currentMode = AI;
Serial.println("Switched to AI Mode.");
lastModeChangeTime = currentMillis;
}
lastManualPinState = manualPinState;
lastAutoPinState = autoPinState;
lastAiPinState = aiPinState;
}
void checkWaterLevel() {
if (digitalRead(waterLevelPin) == HIGH) { // 높은 수위일 때
if (!waterLevelHigh) { // 처음 감지된 경우에만 실행
Serial.println("Water level is high, stopping pump and returning to idle mode.");
stopPumpSequence();
waterLevelHigh = true;
}
} else {
waterLevelHigh = false;
}
}
// checkPumpControl 함수에서 수정
void checkPumpControl() {
unsigned long currentTime = millis();
int smCountHigh = 0;
int smCountLow = 0;
// waterLevelHigh가 true일 경우 제어 상태를 종료합니다
if (waterLevelHigh) {
control_pumpState = false;
return;
}
if (currentMode == MANUAL) {
// 수동 모드에서 control_PumpOn 핀이 HIGH일 때 펌프 작동
if (digitalRead(control_PumpOn) == HIGH && !control_pumpState) {
Serial.println("Manual mode: Turning on the pump");
control_pumpState = true;
} else if (digitalRead(control_PumpOff) == HIGH && control_pumpState) {
Serial.println("Manual mode: Turning off the pump");
control_pumpState = false;
stopPumpSequence();
}
} else if (currentMode == AUTO) {
// ADC0, ADC1, ADC2 변수를 사용해 조건 체크
if (ADC0 > smStart) smCountHigh++;
if (ADC1 > smStart) smCountHigh++;
if (ADC2 > smStart) smCountHigh++;
if (ADC0 < smStop) smCountLow++;
if (ADC1 < smStop) smCountLow++;
if (ADC2 < smStop) smCountLow++;
if (smCountHigh >= 2 && !control_pumpState) {
if (!holdConditionMet) {
holdStartTime = currentTime;
holdConditionMet = true;
}
if (currentTime - holdStartTime >= holdDuration) {
control_pumpState = true;
pumpStartTime = currentTime; // cycleTime 체크 시작
}
} else if (smCountLow >= 2 || digitalRead(rainSensor) == HIGH) {
if (!holdConditionMet) {
holdStartTime = currentTime;
holdConditionMet = true;
}
if (currentTime - holdStartTime >= holdDuration && control_pumpState) {
control_pumpState = false;
stopPumpSequence();
restartDelayStartTime = currentTime; // cycleRestartDelay 체크 시작
}
} else {
holdConditionMet = false;
holdStartTime = 0;
}
}
}
void managePumpSequence() {
unsigned long currentTime = millis();
// 물이 높은 경우 펌프를 즉시 중지하고 리턴
if (digitalRead(waterLevelPin) == HIGH) {
stopPumpSequence();
return;
}
// 펌프 초기화 후 빠르게 재시작하지 않도록 지연
if (delayAfterPumpOff && (currentTime - pumpOffTime < valveDelay)) {
return;
} else {
delayAfterPumpOff = false;
}
// 자동 모드에서 강우 센서가 HIGH인 경우 펌프를 중지
if (currentMode == AUTO && digitalRead(rainSensor) == HIGH) {
stopPumpSequence();
return;
}
// 현재 모드에 따른 동작 처리
switch (currentMode) {
case MANUAL:
handleManualMode();
break;
case AUTO:
handleAutoMode();
break;
case AI:
handleAIMode();
break;
}
}
void handleManualMode() {
// 수동 모드에서 펌프 ON 명령이 들어왔을 때
if (digitalRead(control_PumpOn) == HIGH && !pumpIsOn && !valveOpening) {
Serial.println("Manual mode: Opening valve and waiting for delay");
digitalWrite(valvePin, HIGH); // 밸브를 먼저 열기
valveOpening = true; // 밸브 열림 상태 플래그 설정
valveOpenStartTime = millis(); // 밸브 열림 시작 시간 기록
}
// 밸브가 열리고 대기 시간이 지난 후에 펌프를 켭니다
if (valveOpening && (millis() - valveOpenStartTime >= valveDelay) && digitalRead(valvePin) == HIGH && !pumpIsOn) {
Serial.println("Manual mode: Valve delay complete, turning on pump");
digitalWrite(pumpPin, HIGH); // 펌프 켜기
pumpIsOn = true; // 펌프 상태 플래그 설정
valveOpening = false; // 밸브 상태 초기화
}
// 수동 모드에서 펌프 OFF 명령이 들어왔을 때
if (digitalRead(control_PumpOff) == HIGH && pumpIsOn) {
Serial.println("Manual mode: Turning off the pump and closing valve");
stopPumpSequence(); // 펌프와 밸브 즉시 종료
}
}
void handleAutoMode() {
unsigned long currentMillis = millis();
// 특정 조건이 충족되면 시퀀스를 중단
if (checkExitConditions()) {
Serial.println("Exit condition met, stopping sequence.");
stopPumpSequence(); // 시퀀스를 종료하고 초기 상태로 복귀
currentStep = 1; // 초기 단계로 복귀하여 시퀀스 재시작 준비
return;
}
switch (currentStep) {
case 1:
Serial.println("Step 1: Opening valve");
digitalWrite(valvePin, HIGH); // 밸브 열기
valveOpenStartTime = currentMillis; // 밸브 열림 시작 시간 기록
currentStep = 2; // 다음 단계로 이동
break;
case 2:
if (currentMillis - valveOpenStartTime >= valveDelay) {
Serial.println("Step 2: Valve Delay complete, turning on pump");
digitalWrite(pumpPin, HIGH); // 펌프 켜기
pumpStartTime = currentMillis; // 펌프 시작 시간 기록
pumpIsOn = true; // 펌프 상태 설정
currentStep = 3; // 다음 단계로 이동
}
break;
case 3:
if (currentMillis - pumpStartTime >= cycleTime) {
Serial.println("Step 3: cycleTime complete, turning off pump");
digitalWrite(pumpPin, LOW); // 펌프 끄기
pumpIsOn = false; // 펌프 상태 업데이트
currentStep = 4; // 다음 단계로 이동
}
break;
case 4:
Serial.println("Step 4: Closing valve");
digitalWrite(valvePin, LOW); // 밸브 닫기
valveOpenStartTime = 0;
restartDelayStartTime = currentMillis; // 재시작 대기 시간 기록
currentStep = 5; // 다음 단계로 이동
break;
case 5:
if (currentMillis - restartDelayStartTime >= cycleRestartDelay) {
Serial.println("Step 5: cycleRestartDelay complete, ready to start new sequence");
currentStep = 1; // 시퀀스를 처음 단계로 복귀
}
break;
}
}
// checkExitConditions 함수에서 수정
bool checkExitConditions() {
int smStopCount = 0;
// ADC0, ADC1, ADC2 변수를 사용하여 smStop 조건 확인
if (ADC0 < smStop) smStopCount++;
if (ADC1 < smStop) smStopCount++;
if (ADC2 < smStop) smStopCount++;
bool waterLevelHigh = digitalRead(waterLevelPin) == HIGH;
bool rainDetected = digitalRead(rainSensor) == HIGH;
return (smStopCount >= 2 || waterLevelHigh || rainDetected);
}
void handleAIMode() {
unsigned long currentMillis = millis();
// AI 모드에서 펌프를 켜야 하는 경우
if (aiModeRequested && !pumpIsOn) {
// 밸브가 열리는 상태가 아니라면 밸브 열기
if (!valveOpening) {
Serial.println("AI Mode: Opening valve and starting delay.");
digitalWrite(valvePin, HIGH); // 밸브 열기
valveOpening = true; // 밸브 열림 상태 플래그 설정
valveOpenStartTime = currentMillis; // 밸브 열림 시간 기록
}
// 밸브가 열린 후 지연 시간이 지나면 펌프를 켜기
else if (currentMillis - valveOpenStartTime >= valveDelay) {
Serial.println("AI Mode: Turning on the pump after valve delay.");
digitalWrite(pumpPin, HIGH); // 펌프 켜기
pumpIsOn = true; // 펌프 상태 업데이트
valveOpening = false; // 밸브 열림 상태 플래그 해제
valveOpenStartTime = 0; // 밸브 지연 시간 초기화
}
}
// AI 모드가 중단되면 (제어 명령 2,0)
else if (!aiModeRequested && (pumpIsOn || valveOpening)) {
Serial.println("AI Mode: Turning off the pump and closing valve.");
stopPumpSequence(); // 펌프와 밸브를 즉시 중지
}
}
void handleControlCommand(int command, int value) {
if (command == 1) { // 모드 제어 명령
if (value == 0) {
currentMode = MANUAL;
Serial.println("Control Command: Manual Mode activated.");
} else if (value == 1) {
currentMode = AUTO;
Serial.println("Control Command: Auto Mode activated.");
} else if (value == 2) {
currentMode = AI;
Serial.println("Control Command: AI Mode activated.");
}
stopPumpSequence(); // 모드 전환 시 펌프 초기화
} else if (command == 2) { // 펌프 제어 명령
if (value == 0) {
aiModeRequested = false;
stopPumpSequence();
Serial.println("Control Command: Pump off.");
} else if (value == 1) {
aiModeRequested = true;
if (currentMode == AI) {
handleAIMode();
} else if (currentMode == AUTO) {
handleAutoMode();
}
}
}
}
void stopPumpSequence() {
Serial.println("Stopping pump sequence. Turning off pump and closing valve.");
digitalWrite(pumpPin, LOW); // 펌프 끄기
digitalWrite(valvePin, LOW); // 밸브 닫기
pumpIsOn = false; // 펌프 상태 플래그 설정
valveOpening = false; // 밸브 열림 상태 플래그 해제
aiModeRequested = false; // AI 모드 요청 플래그 해제
control_pumpState = false; // 제어 플래그 해제
valveOpenStartTime = 0; // 밸브 열림 시간 초기화
currentStep = 1; // 시퀀스를 초기 단계로 복귀
}
void sendSensorDataToMaster() {
// SensorData 업데이트
updateSoilMoistureValues(); // 먼저 토양 수분 값 업데이트
sensorData.ec = ec; // EC 값
sensorData.ph = ph * 100; // PH 값 (곱하기 100을 통해 정밀도 조정)
sensorData.airTemperature = airtemperature * 100; // 대기 온도 값 (곱하기 100을 통해 정밀도 조정)
sensorData.airHumidity = airhumidity * 100; // 대기 습도 값 (곱하기 100을 통해 정밀도 조정)
sensorData.ADC0 = ADC0; // 업데이트된 토양 수분 센서 값 (ADC0)
sensorData.ADC1 = ADC1; // 업데이트된 토양 수분 센서 값 (ADC1)
sensorData.ADC2 = ADC2; // 업데이트된 토양 수분 센서 값 (ADC2, 85 추가 적용)
sensorData.soilTemp1 = soilTemp[0] * 100; // 첫 번째 DS18B20 센서 값 (곱하기 100)
sensorData.soilTemp2 = soilTemp[1] * 100; // 두 번째 DS18B20 센서 값 (곱하기 100)
sensorData.l_minute = l_minute; // 유량 센서 값 (L/min 단위)
sensorData.pumpPin = digitalRead(pumpPin); // 펌프 핀의 현재 상태 (HIGH 또는 LOW)
sensorData.valvePin = digitalRead(valvePin); // 밸브 핀의 현재 상태 (HIGH 또는 LOW)
sensorData.waterLevelPin = digitalRead(waterLevelPin); // 수위 센서 핀의 상태 (HIGH 또는 LOW)
sensorData.rainSensor = digitalRead(rainSensor); // 강우 센서 핀의 상태 (HIGH 또는 LOW)
/// CRC 계산
sensorData.crc = calculateCRC((uint8_t*)&sensorData, sizeof(SensorData) - sizeof(sensorData.crc));
// 데이터 송신
Wire.write((uint8_t*)&sensorData, sizeof(SensorData));
}
void sendConfigDataToMaster() {
// currentMode 업데이트
/*
// configData 구조체의 크기를 출력하여 확인
Serial.print("Size of ConfigData structure: ");
Serial.println(sizeof(ConfigData));
Serial.print( "Current Mode: " );
Serial.println( currentMode );
*/
configData.currentMode = currentMode == MANUAL ? 0 : (currentMode == AUTO ? 1 : 2);
// 시간 관련 설정 값들을 초 단위로 변환하여 업데이트
configData.cycleTime = cycleTime;
configData.cycleRestartDelay = cycleRestartDelay;
configData.valveDelay = valveDelay;
// 토양 수분 설정 값들 업데이트
configData.smStart = smStart;
configData.smStop = smStop;
//configData.holdDuration = holdDuration / 1000;
// CRC 계산
configData.crc = calculateCRC((uint8_t*)&configData, sizeof(ConfigData) - sizeof(configData.crc));
/*
// 디버깅: 전송 전에 ConfigData 내용을 시리얼로 출력 (선택사항)
Serial.println("Sending ConfigData to Master:");
Serial.print("Current Mode: "); Serial.println(configData.currentMode);
Serial.print("Cycle Time (s): "); Serial.println(configData.cycleTime);
Serial.print("Cycle Restart Delay (s): "); Serial.println(configData.cycleRestartDelay);
Serial.print("Valve Delay (s): "); Serial.println(configData.valveDelay);
Serial.print("Soil Moisture Start Threshold: "); Serial.println(configData.smStart);
Serial.print("Soil Moisture Stop Threshold: "); Serial.println(configData.smStop);
//Serial.print("Hold Duration (s): "); Serial.println(configData.holdDuration);
Serial.print("CRC: "); Serial.println(configData.crc, HEX);
*/
// I2C를 통해 ConfigData 전송
Wire.write((uint8_t*)&configData, sizeof(ConfigData));
}
void onRequestEvent() {
unsigned long currentMillis = millis();
// 데이터 전송 간격이 충분히 지났는지 확인
if ((currentMillis - lastSendTime >= sendInterval) || lastSendTime == 0) {
if (sendSensorNext) {
sendSensorDataToMaster();
sendSensorNext = false;
} else {
sendConfigDataToMaster();
sendSensorNext = true;
}
lastSendTime = currentMillis; // 전송 후 시간 갱신
} else {
Serial.println("Not enough time has passed to send data.");
}
}
// receiveData 함수에서 업데이트
// receiveData 함수에서 업데이트
void receiveData(int numBytes) {
int settingID = Wire.read();
int lowerByte = Wire.read();
int upperByte = Wire.read();
uint32_t settingValue = (upperByte << 8) | lowerByte;
Serial.print("Received setting ID: ");
Serial.println(settingID);
Serial.print("Setting Value: ");
Serial.println(settingValue);
switch (settingID) {
case 1: // currentMode 설정 0 == 수동, 1 == 자동, 2 == AI
if (settingValue == 0) {
currentMode = MANUAL;
Serial.println("Switched to Manual Mode.");
} else if (settingValue == 1) {
currentMode = AUTO;
Serial.println("Switched to Auto Mode.");
} else if (settingValue == 2) {
currentMode = AI;
Serial.println("Switched to AI Mode.");
} else {
Serial.println("Invalid mode value received for setting ID 1.");
}
stopPumpSequence(); // 모드 전환 시 펌프 초기화
break;
case 2: // AI 모드에서 펌프 제어
if (currentMode == AI) {
aiModeRequested = (settingValue == 1);
Serial.print("AI Mode: Received command to turn ");
Serial.println(aiModeRequested ? "ON" : "OFF");
handleAIMode();
} else {
Serial.println("Pump control command received, but system is not in AI mode.");
}
break;
case 3: // cycleTime 설정 (펌프 ON 지속시간)
{
unsigned long calculatedCycleTime = settingValue * 1000;
cycleTime = (calculatedCycleTime > 86400000UL) ? 86400000UL : calculatedCycleTime;
configData.cycleTime = cycleTime;
Serial.print("Updated cycleTime: ");
Serial.println(cycleTime);
}
break;
case 4: // cycleRestartDelay 설정 (펌프 OFF 지속시간)
{
unsigned long calculatedRestartDelay = settingValue * 1000UL; // 밀리초 단위 변환
cycleRestartDelay = (calculatedRestartDelay > 86400000UL) ? 86400000UL : calculatedRestartDelay;
configData.cycleRestartDelay = cycleRestartDelay;
Serial.print("Updated cycleRestartDelay: ");
Serial.println(cycleRestartDelay);
}
break;
case 5: // valveDelay 설정
{
unsigned long calculatedDelay = settingValue * 1000;
valveDelay = (calculatedDelay > 86400000UL) ? 86400000UL : calculatedDelay;
configData.valveDelay = valveDelay;
Serial.print("Updated valveDelay: ");
Serial.println(valveDelay);
}
break;
case 6: // smStart 설정
smStart = (settingValue > 1023) ? 1023 : settingValue;
configData.smStart = smStart;
Serial.print("Updated smStart: ");
Serial.println(smStart);
break;
case 7: // smStop 설정
smStop = (settingValue > 1023) ? 1023 : settingValue;
configData.smStop = smStop;
Serial.print("Updated smStop: ");
Serial.println(smStop);
break;
case 8: // holdDuration 설정
{
unsigned long calculatedHoldDuration = settingValue * 1000;
holdDuration = (calculatedHoldDuration > 86400000UL) ? 86400000UL : calculatedHoldDuration;
Serial.print("Updated holdDuration: ");
Serial.println(holdDuration);
}
break;
default:
Serial.println("Unknown setting ID received");
break;
}
// CRC 업데이트 후 설정 데이터 전송
configData.crc = calculateCRC((uint8_t*)&configData, sizeof(ConfigData) - sizeof(configData.crc));
//Serial.print("Updated CRC: ");
//Serial.println(configData.crc, HEX);
// ConfigData를 마스터로 전송
sendConfigDataToMaster();
}
void printSensorData() {
unsigned long currentTime = millis();
// 2초 간격으로 센서 및 플래그 상태를 출력
if (currentTime - lastSensorPrintTime >= sensorPrintInterval) {
lastSensorPrintTime = currentTime;
// 토양 수분 센서 값 출력
Serial.print("Soil Moisture Sensor Values: ");
Serial.print("ADC0: "); Serial.print(ADC0);
Serial.print(", ADC1: "); Serial.print(ADC1);
Serial.print(", ADC2: "); Serial.println(ADC2);
Serial.println();
Serial.print("SoilTemp 1: ");
Serial.print(soilTemp[0]);
Serial.print(", ");
Serial.print("SoilTemp 2: ");
Serial.println(soilTemp[1]);
// water level, rain sensor 상태 출력
Serial.print("Water Level: ");
Serial.print(digitalRead(waterLevelPin) == HIGH ? "High" : "Low");
Serial.print(", Rain Sensor: ");
Serial.println(digitalRead(rainSensor) == HIGH ? "Detected" : "Not Detected");
// 유량 정보 출력
calculateFlowRate(); // 1초마다 유량 계산하여 출력
// 각 플래그 및 상태 변수 출력
Serial.print("Current Mode: ");
switch (currentMode) {
case MANUAL:
Serial.println("Manual");
break;
case AUTO:
Serial.println("Auto");
break;
case AI:
Serial.println("AI");
break;
}
Serial.print(",Pump: ");
Serial.print(pumpIsOn ? "ON" : "OFF");
Serial.print(",Valve: ");
Serial.print(valveOpening ? "Opening" : "Closed");
Serial.print(",ControlPump: ");
Serial.print(control_pumpState ? "ON" : "OFF");
Serial.print(",Water High: ");
Serial.print(waterLevelHigh ? "True" : "False");
Serial.print(",In Cycle: ");
Serial.print(inCycle ? "True" : "False");
Serial.print(",AutoModeAlerted: ");
Serial.println(autoModeAlerted ? "True" : "False");
Serial.println("-------------------------------------------------");
}
}
void setup() {
Serial.begin(115200);
Wire.begin(8);
Wire.setClock(100000); // I2C 속도를 400kHz로 설정 (고속 모드) 예를 들어, 100000은 100kHz (기본), 400000은 400kHz (고속)
Serial2.begin(9600); // RS485 통신 포트 초기화
node1.begin(1, Serial2); // RS485 슬레이브 ID 1
node2.begin(2, Serial2); // RS485 슬레이브 ID 2
Wire.onRequest(onRequestEvent); // 요청 시 이벤트 처리
Wire.onReceive(receiveData);
sensors.begin(); // DS18B20 센서 초기화
pinMode(pumpPin, OUTPUT);
pinMode(valvePin, OUTPUT);
pinMode(control_PumpOn, INPUT);
pinMode(control_PumpOff, INPUT);
pinMode(waterLevelPin, INPUT);
pinMode(rainSensor, INPUT);
pinMode(flowSensorPin, INPUT);
pinMode(manualModePin, INPUT);
pinMode(autoModePin, INPUT);
pinMode(aiModePin, INPUT);
digitalWrite(flowSensorPin, HIGH); // 내장 풀업 설정
attachInterrupt(digitalPinToInterrupt(flowSensorPin), flow, RISING); // 유량 센서 인터럽트 설정
analogReference(EXTERNAL);
digitalWrite(pumpPin, LOW); // 초기 펌프 OFF
digitalWrite(valvePin, LOW); // 초기 밸브 CLOSE
// lastSendTime을 현재 시간에서 초기화하여 첫 전송이 즉시 발생하게 함
lastSendTime = millis() - sendInterval; // sendInterval 간격을 맞춰 첫 전송 가능
// 초기 설정 값 출력
Serial.println("System Initialization:");
Serial.print("Initial cycleTime: ");
Serial.println(cycleTime);
Serial.print("Initial cycleRestartDelay: ");
Serial.println(cycleRestartDelay);
Serial.println("System initialized in Manual mode.");
}
void loop() {
unsigned long currentMillis = millis();
// 주기적으로 센서 데이터를 읽습니다
if (currentMillis - lastRequestTime >= requestInterval) {
lastRequestTime = currentMillis;
// ID 1 데이터 읽기
readSensorDataID1();
// ID 1 요청 후 짧은 대기 후 ID 2 데이터 읽기
delay(50); // 50ms 대기 (필요한 최소한의 대기 시간)
readSensorDataID2();
}
updateSoilMoistureValues(); // 토양 수분 값 업데이트
checkMode(); // 모드 확인 및 전환
checkWaterLevel(); // 워터 레벨 상태 확인
checkPumpControl(); // 펌프 제어 상태 확인
managePumpSequence(); // 펌프 시퀀스 관리
readDS18B20Data(); // 온도 센서 데이터 읽기
calculateFlowRate();
//printSensorData(); // 센서 및 상태 출력
}

File diff suppressed because it is too large Load Diff