First
This commit is contained in:
commit
970680570e
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.pio
|
||||
.vscode
|
||||
include/README
|
||||
test/README
|
||||
46
lib/README
Normal file
46
lib/README
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into the executable file.
|
||||
|
||||
The source code of each library should be placed in a separate directory
|
||||
("lib/your_library_name/[Code]").
|
||||
|
||||
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
Example contents of `src/main.c` using Foo and Bar:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries by scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
14
platformio.ini
Normal file
14
platformio.ini
Normal file
@ -0,0 +1,14 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:heltec_wireless_tracker_v12]
|
||||
platform = espressif32
|
||||
board = heltec_wireless_tracker_v12
|
||||
framework = arduino
|
||||
484
src/Receiver/main.cpp
Normal file
484
src/Receiver/main.cpp
Normal file
@ -0,0 +1,484 @@
|
||||
/*
|
||||
프로젝트명: GNSS + LoRa + MQTT 실시간 위치 및 환경 데이터 수신 시스템
|
||||
보드 종류: ESP32 (예: HELTEC Wireless Tracker 또는 유사 보드)
|
||||
REV 2.0
|
||||
|
||||
주요 기능:
|
||||
- GNSS(GPS) 수신 (TinyGPS++)
|
||||
- LoRa 데이터 수신 및 JSON 파싱 (RadioLib, ArduinoJson)
|
||||
- WiFi 신호 세기 기반 자동 연결 및 후보 관리 (Preferences EEPROM 저장)
|
||||
- MQTT 브로커로 SELF 및 NODE1 데이터 번갈아 전송 (PubSubClient)
|
||||
- OLED TFT 디스플레이 출력 (HT_st7735)
|
||||
- 시리얼 명령어를 통한 WiFi 및 MQTT 설정 관리
|
||||
- 설정 모드에서 WiFi/MQTT 정보 저장 및 복원
|
||||
- TFT 출력 시 고정 길이 문자열 포맷 적용으로 잔여 문자 문제 방지
|
||||
|
||||
시리얼 명령어:
|
||||
- CONFIG : 설정 모드 진입 (시스템 주요 기능 일시 중지)
|
||||
- LIST : 저장된 WiFi 후보 목록 출력
|
||||
- ADD ssid,pass : WiFi 후보 추가 (최대 후보 개수 제한)
|
||||
- EDIT idx ssid,pass : 지정 인덱스 WiFi 후보 수정
|
||||
- DELETE idx : 지정 인덱스 WiFi 후보 삭제
|
||||
- MQTTSET server,user,pass,topic : MQTT 설정 저장
|
||||
- MQTTINFO : 현재 MQTT 설정 출력
|
||||
- EXIT : 설정 모드 종료 및 시스템 재시작
|
||||
|
||||
TFT 디스플레이 정보:
|
||||
- SELF 위치 정보 및 GPS 위성 상태 녹색 표시
|
||||
- NODE1 수신 데이터 (TVOC, eCO2 포함) 노란색 표시
|
||||
- CONFIG 모드 진입 시 안내 메시지 표시
|
||||
|
||||
하드웨어 연결:
|
||||
- GNSS_TX (GPS 송신) : GPIO 33
|
||||
- GNSS_RX (GPS 수신) : GPIO 34
|
||||
- VGNSS_CTRL (GPS 전원 제어) : GPIO 3
|
||||
- LoRa SPI 핀 (SX1262) : NSS=8, DIO1=14, RESET=12, BUSY=13
|
||||
|
||||
LoRa 설정:
|
||||
- 주파수: 923.0 MHz
|
||||
- Spreading Factor: 7
|
||||
- Bandwidth: 125 kHz
|
||||
- Coding Rate: 4/5
|
||||
- Sync Word: 0x12
|
||||
|
||||
저장소:
|
||||
- WiFi 설정: Preferences 네임스페이스 "wifi"
|
||||
- MQTT 설정: Preferences 네임스페이스 "mqtt"
|
||||
|
||||
개발 및 테스트 환경:
|
||||
- Arduino IDE 및 ESP32 환경
|
||||
- 안정적인 GPS 신호 확보를 위한 별도 초기화 및 수신 처리
|
||||
- 주기적 WiFi 및 MQTT 연결 상태 점검 및 자동 복구
|
||||
- JSON 파싱 오류 및 TFT 출력 잔여 문자 문제 방지를 위한 고정 길이 문자열 포맷 적용
|
||||
|
||||
작성일: 2025-07-29
|
||||
작성자: BK Choi
|
||||
*/
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "HT_st7735.h"
|
||||
#include "HT_TinyGPS++.h"
|
||||
#include <RadioLib.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <esp_mac.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
#define GNSS_TX 33
|
||||
#define GNSS_RX 34
|
||||
#define VGNSS_CTRL 3
|
||||
#define MAX_WIFI_CANDIDATES 10
|
||||
|
||||
HT_st7735 tft;
|
||||
TinyGPSPlus GPS;
|
||||
SX1262 radio = new Module(8, 14, 12, 13);
|
||||
|
||||
struct WiFiCandidate {
|
||||
String ssid;
|
||||
String password;
|
||||
};
|
||||
WiFiCandidate wifiList[MAX_WIFI_CANDIDATES];
|
||||
int wifiCount = 0;
|
||||
|
||||
String mqtt_server = "selimcns.synology.me";
|
||||
String mqtt_user = "";
|
||||
String mqtt_password = "";
|
||||
String mqtt_topic = "odor/monitor";
|
||||
Preferences prefs;
|
||||
WiFiClient espClient;
|
||||
PubSubClient mqttClient(espClient);
|
||||
|
||||
volatile bool receivedFlag = false;
|
||||
String latestSELF = "";
|
||||
String latestNODE1 = "";
|
||||
|
||||
bool configMode = false;
|
||||
|
||||
enum State { S_SELF,
|
||||
S_NODE1 };
|
||||
State publishState = S_SELF;
|
||||
unsigned long lastNode1Time = 0;
|
||||
const unsigned long NODE1_TIMEOUT = 10000;
|
||||
|
||||
void setFlag() {
|
||||
receivedFlag = true;
|
||||
}
|
||||
|
||||
void saveWiFiListToEEPROM() {
|
||||
char key[16];
|
||||
prefs.begin("wifi", false);
|
||||
prefs.putUInt("count", wifiCount);
|
||||
for (int i = 0; i < wifiCount; i++) {
|
||||
snprintf(key, sizeof(key), "ssid%d", i);
|
||||
prefs.putString(key, wifiList[i].ssid);
|
||||
snprintf(key, sizeof(key), "pass%d", i);
|
||||
prefs.putString(key, wifiList[i].password);
|
||||
}
|
||||
prefs.end();
|
||||
}
|
||||
|
||||
void loadWiFiListFromEEPROM() {
|
||||
char key[16];
|
||||
prefs.begin("wifi", true);
|
||||
wifiCount = prefs.getUInt("count", 0);
|
||||
for (int i = 0; i < wifiCount && i < MAX_WIFI_CANDIDATES; i++) {
|
||||
snprintf(key, sizeof(key), "ssid%d", i);
|
||||
wifiList[i].ssid = prefs.getString(key, "");
|
||||
snprintf(key, sizeof(key), "pass%d", i);
|
||||
wifiList[i].password = prefs.getString(key, "");
|
||||
}
|
||||
prefs.end();
|
||||
}
|
||||
|
||||
void saveMQTTToEEPROM() {
|
||||
prefs.begin("mqtt", false);
|
||||
prefs.putString("server", mqtt_server);
|
||||
prefs.putString("user", mqtt_user);
|
||||
prefs.putString("pass", mqtt_password);
|
||||
prefs.putString("topic", mqtt_topic);
|
||||
prefs.end();
|
||||
}
|
||||
|
||||
void loadMQTTFromEEPROM() {
|
||||
prefs.begin("mqtt", true);
|
||||
mqtt_server = prefs.getString("server", mqtt_server);
|
||||
mqtt_user = prefs.getString("user", mqtt_user);
|
||||
mqtt_password = prefs.getString("pass", mqtt_password);
|
||||
mqtt_topic = prefs.getString("topic", mqtt_topic);
|
||||
prefs.end();
|
||||
}
|
||||
|
||||
void handleSerialCommand() {
|
||||
if (Serial.available()) {
|
||||
String cmd = Serial.readStringUntil('\n');
|
||||
cmd.trim();
|
||||
|
||||
if (cmd == "CONFIG") {
|
||||
configMode = true;
|
||||
Serial.println("[설정 모드 진입: 기능 일시 중지됨]");
|
||||
Serial.println("명령어: LIST, ADD ssid,pass, EDIT idx ssid,pass, DELETE idx");
|
||||
Serial.println("MQTTSET server,user,pass,topic / MQTTINFO / EXIT");
|
||||
|
||||
// TFT 출력 추가
|
||||
tft.st7735_fill_screen(ST7735_BLACK);
|
||||
tft.st7735_write_str(0, 0, "CONFIG MODE", Font_7x10, ST7735_RED, ST7735_BLACK);
|
||||
tft.st7735_write_str(0, 12, "Use serial cmd", Font_7x10, ST7735_YELLOW, ST7735_BLACK);
|
||||
} else if (configMode) {
|
||||
if (cmd == "LIST") {
|
||||
Serial.println("[WiFi 목록]");
|
||||
for (int i = 0; i < wifiCount; i++) {
|
||||
Serial.printf("%d. %s / %s\n", i, wifiList[i].ssid.c_str(), wifiList[i].password.c_str());
|
||||
}
|
||||
} else if (cmd.startsWith("ADD ")) {
|
||||
int sep = cmd.indexOf(',');
|
||||
if (sep == -1 || wifiCount >= MAX_WIFI_CANDIDATES) {
|
||||
Serial.println("형식: ADD SSID,PASSWORD");
|
||||
return;
|
||||
}
|
||||
String ssid = cmd.substring(4, sep);
|
||||
String pass = cmd.substring(sep + 1);
|
||||
wifiList[wifiCount++] = { ssid, pass };
|
||||
Serial.println("[WiFi 추가됨]");
|
||||
saveWiFiListToEEPROM();
|
||||
} else if (cmd.startsWith("EDIT ")) {
|
||||
int sep1 = cmd.indexOf(' ', 5);
|
||||
int sep2 = cmd.indexOf(',', sep1);
|
||||
if (sep1 == -1 || sep2 == -1) {
|
||||
Serial.println("형식: EDIT INDEX SSID,PASSWORD");
|
||||
return;
|
||||
}
|
||||
int idx = cmd.substring(5, sep1).toInt();
|
||||
if (idx < 0 || idx >= wifiCount) {
|
||||
Serial.println("잘못된 인덱스");
|
||||
return;
|
||||
}
|
||||
String ssid = cmd.substring(sep1 + 1, sep2);
|
||||
String pass = cmd.substring(sep2 + 1);
|
||||
wifiList[idx] = { ssid, pass };
|
||||
Serial.println("[WiFi 수정됨]");
|
||||
saveWiFiListToEEPROM();
|
||||
} else if (cmd.startsWith("DELETE ")) {
|
||||
int idx = cmd.substring(7).toInt();
|
||||
if (idx < 0 || idx >= wifiCount) {
|
||||
Serial.println("잘못된 인덱스");
|
||||
return;
|
||||
}
|
||||
for (int i = idx; i < wifiCount - 1; i++) wifiList[i] = wifiList[i + 1];
|
||||
wifiCount--;
|
||||
Serial.println("[WiFi 삭제됨]");
|
||||
saveWiFiListToEEPROM();
|
||||
} else if (cmd.startsWith("MQTTSET ")) {
|
||||
int sep1 = cmd.indexOf(',', 8);
|
||||
int sep2 = cmd.indexOf(',', sep1 + 1);
|
||||
int sep3 = cmd.indexOf(',', sep2 + 1);
|
||||
if (sep1 == -1 || sep2 == -1 || sep3 == -1) {
|
||||
Serial.println("형식: MQTTSET server,user,pass,topic");
|
||||
return;
|
||||
}
|
||||
mqtt_server = cmd.substring(8, sep1);
|
||||
mqtt_user = cmd.substring(sep1 + 1, sep2);
|
||||
mqtt_password = cmd.substring(sep2 + 1, sep3);
|
||||
mqtt_topic = cmd.substring(sep3 + 1);
|
||||
saveMQTTToEEPROM();
|
||||
Serial.println("[MQTT 설정 저장됨]");
|
||||
} else if (cmd == "MQTTINFO") {
|
||||
Serial.println("[MQTT 설정 정보]");
|
||||
Serial.println("Server: " + mqtt_server);
|
||||
Serial.println("User: " + mqtt_user);
|
||||
Serial.println("Password: " + mqtt_password);
|
||||
Serial.println("Topic: " + mqtt_topic);
|
||||
} else if (cmd == "EXIT") {
|
||||
configMode = false;
|
||||
Serial.println("[설정 모드 종료: 시스템 재시작 중...]");
|
||||
tft.st7735_fill_screen(ST7735_BLACK); // 화면 초기화
|
||||
ESP.restart();
|
||||
} else {
|
||||
Serial.println("지원 명령어: LIST, ADD, EDIT, DELETE, MQTTSET, MQTTINFO, EXIT");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void connectToBestWiFi() {
|
||||
int bestRSSI = -1000, bestIndex = -1;
|
||||
bool openWiFiAttempted = false;
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
int n = WiFi.scanNetworks();
|
||||
if (n == 0) {
|
||||
Serial.println("[WiFi 검색 실패] - CONFIG 입력으로 설정 가능");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
for (int j = 0; j < wifiCount; j++) {
|
||||
if (WiFi.SSID(i) == wifiList[j].ssid && WiFi.RSSI(i) > bestRSSI) {
|
||||
bestRSSI = WiFi.RSSI(i);
|
||||
bestIndex = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestIndex != -1) {
|
||||
WiFi.begin(wifiList[bestIndex].ssid.c_str(), wifiList[bestIndex].password.c_str());
|
||||
unsigned long start = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) delay(500);
|
||||
if (WiFi.status() == WL_CONNECTED) return;
|
||||
} else {
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (WiFi.encryptionType(i) == WIFI_AUTH_OPEN && !openWiFiAttempted) {
|
||||
WiFi.begin(WiFi.SSID(i).c_str());
|
||||
unsigned long start = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) delay(500);
|
||||
if (WiFi.status() == WL_CONNECTED) return;
|
||||
openWiFiAttempted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(3000);
|
||||
}
|
||||
Serial.println("[WiFi 연결 실패] 설정을 확인해주세요");
|
||||
}
|
||||
|
||||
String generateClientID() {
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
char macStr[13];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sprintf(&macStr[i * 2], "%02X", mac[i]);
|
||||
}
|
||||
return "DEVICE" + String(macStr) + String(random(1000, 9999));
|
||||
}
|
||||
|
||||
void connectToMQTT() {
|
||||
mqttClient.setServer(mqtt_server.c_str(), 1883);
|
||||
String clientId = generateClientID();
|
||||
unsigned long start = millis();
|
||||
while (!mqttClient.connected() && millis() - start < 10000) {
|
||||
mqttClient.connect(clientId.c_str(), mqtt_user.c_str(), mqtt_password.c_str());
|
||||
delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
void ensureWiFiAndMQTTConnected() {
|
||||
static unsigned long lastCheck = 0;
|
||||
if (millis() - lastCheck < 5000) return;
|
||||
lastCheck = millis();
|
||||
if (WiFi.status() != WL_CONNECTED) connectToBestWiFi();
|
||||
if (!mqttClient.connected()) connectToMQTT();
|
||||
mqttClient.loop();
|
||||
}
|
||||
|
||||
void handleGPSAndSelfJson() {
|
||||
while (!configMode && Serial1.available()) GPS.encode(Serial1.read());
|
||||
static unsigned long lastSelfTime = 0;
|
||||
if (!configMode && millis() - lastSelfTime > 1000) {
|
||||
lastSelfTime = millis();
|
||||
StaticJsonDocument<256> doc;
|
||||
char timeStr[10];
|
||||
sprintf(timeStr, "%02d:%02d:%02d", (GPS.time.hour() + 9) % 24, GPS.time.minute(), GPS.time.second());
|
||||
doc["id"] = "SELF";
|
||||
doc["time"] = timeStr;
|
||||
doc["lat"] = GPS.location.lat();
|
||||
doc["lon"] = GPS.location.lng();
|
||||
doc["alt"] = GPS.altitude.meters();
|
||||
doc["spd"] = GPS.speed.kmph();
|
||||
doc["sat"] = GPS.satellites.value();
|
||||
doc["hdop"] = GPS.hdop.hdop();
|
||||
char buffer[256];
|
||||
serializeJson(doc, buffer);
|
||||
latestSELF = String(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void handleLoRaReceive() {
|
||||
if (!configMode && receivedFlag) {
|
||||
receivedFlag = false;
|
||||
String recv;
|
||||
if (radio.readData(recv) == RADIOLIB_ERR_NONE) {
|
||||
if (recv.indexOf("\"id\":\"NODE-001\"") != -1) {
|
||||
latestNODE1 = recv;
|
||||
lastNode1Time = millis();
|
||||
Serial.println("[LoRa 수신]: " + recv);
|
||||
}
|
||||
}
|
||||
radio.startReceive();
|
||||
}
|
||||
}
|
||||
|
||||
void handleMQTTPublish() {
|
||||
static unsigned long lastPub = 0;
|
||||
if (!configMode && millis() - lastPub > 1000) {
|
||||
lastPub = millis();
|
||||
if (millis() - lastNode1Time > NODE1_TIMEOUT) latestNODE1 = "";
|
||||
String toSend = "";
|
||||
if (publishState == S_SELF && latestSELF != "") {
|
||||
toSend = latestSELF;
|
||||
publishState = S_NODE1;
|
||||
} else if (publishState == S_NODE1 && latestNODE1 != "") {
|
||||
toSend = latestNODE1;
|
||||
publishState = S_SELF;
|
||||
} else {
|
||||
publishState = (State)(((int)publishState + 1) % 2);
|
||||
}
|
||||
if (toSend != "") {
|
||||
mqttClient.publish(mqtt_topic.c_str(), toSend.c_str());
|
||||
Serial.println("[MQTT 발행]: " + toSend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleTFTDisplay() {
|
||||
if (configMode) return;
|
||||
|
||||
// SELF 정보 출력
|
||||
StaticJsonDocument<256> doc;
|
||||
deserializeJson(doc, latestSELF);
|
||||
char buf[40];
|
||||
sprintf(buf, "SELF %s", doc["time"] | "-");
|
||||
tft.st7735_write_str(0, 0, buf, Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
sprintf(buf, "LAT:%.4f LON:%.4f", doc["lat"] | 0.0, doc["lon"] | 0.0);
|
||||
tft.st7735_write_str(0, 12, buf, Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
sprintf(buf, "SAT:%d HDOP:%.1f", doc["sat"] | 0, doc["hdop"] | 0.0);
|
||||
tft.st7735_write_str(0, 24, buf, Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
|
||||
// NODE1 정보 출력
|
||||
if (latestNODE1 != "") {
|
||||
StaticJsonDocument<256> ndoc;
|
||||
DeserializationError err = deserializeJson(ndoc, latestNODE1);
|
||||
|
||||
if (!err) {
|
||||
sprintf(buf, "NODE1 %s", ndoc["time"] | "-");
|
||||
tft.st7735_write_str(0, 36, buf, Font_7x10, ST7735_YELLOW, ST7735_BLACK);
|
||||
|
||||
sprintf(buf, "LAT:%.4f LON:%.4f", ndoc["lat"] | 0.0, ndoc["lon"] | 0.0);
|
||||
tft.st7735_write_str(0, 48, buf, Font_7x10, ST7735_YELLOW, ST7735_BLACK);
|
||||
|
||||
// 안전한 타입 검사 및 고정 길이 포맷 적용
|
||||
int tvoc = ndoc["tvoc"].is<int>() ? ndoc["tvoc"].as<int>() : -1;
|
||||
int eco2 = ndoc["eco2"].is<int>() ? ndoc["eco2"].as<int>() : -1;
|
||||
|
||||
if (eco2 < 0 || eco2 > 4000) {
|
||||
// 'ERR' 출력 뒤 공백 2칸 추가해 잔여 문자 덮기
|
||||
sprintf(buf, "TVOC:%-5d eCO2:ERR ", tvoc);
|
||||
} else {
|
||||
// 숫자 5자리 확보, 빈 자리 공백으로 채움
|
||||
sprintf(buf, "TVOC:%-5d eCO2:%-5d", tvoc, eco2);
|
||||
}
|
||||
tft.st7735_write_str(0, 60, buf, Font_7x10, ST7735_YELLOW, ST7735_BLACK);
|
||||
} else {
|
||||
tft.st7735_write_str(0, 36, "NODE1 JSON ERR ", Font_7x10, ST7735_RED, ST7735_BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println("\n[시스템 시작]");
|
||||
|
||||
pinMode(VGNSS_CTRL, OUTPUT);
|
||||
digitalWrite(VGNSS_CTRL, HIGH);
|
||||
Serial.println("[GNSS 전원 ON]");
|
||||
|
||||
tft.st7735_init();
|
||||
tft.st7735_fill_screen(ST7735_BLACK);
|
||||
tft.st7735_write_str(0, 0, "Starting...", Font_7x10, ST7735_WHITE, ST7735_BLACK);
|
||||
|
||||
Serial1.begin(115200, SERIAL_8N1, GNSS_TX, GNSS_RX);
|
||||
Serial.println("[GNSS 시리얼 시작됨]");
|
||||
|
||||
loadWiFiListFromEEPROM();
|
||||
Serial.printf("[WiFi 목록 %d개 로드됨]\n", wifiCount);
|
||||
|
||||
loadMQTTFromEEPROM();
|
||||
Serial.println("[MQTT 설정 로드됨]");
|
||||
|
||||
Serial.println("[LoRa 초기화 중...]");
|
||||
if (radio.begin(923.0) == RADIOLIB_ERR_NONE) {
|
||||
radio.setSpreadingFactor(7);
|
||||
radio.setBandwidth(125.0);
|
||||
radio.setCodingRate(5);
|
||||
radio.setSyncWord(0x12);
|
||||
radio.setDio1Action(setFlag);
|
||||
radio.startReceive();
|
||||
Serial.println("[LoRa 시작됨]");
|
||||
tft.st7735_write_str(0, 0, "LoRa OK", Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
} else {
|
||||
Serial.println("[LoRa 시작 실패]");
|
||||
tft.st7735_write_str(0, 0, "LoRa Fail", Font_7x10, ST7735_RED, ST7735_BLACK);
|
||||
while (true)
|
||||
;
|
||||
}
|
||||
|
||||
Serial.println("[WiFi 연결 시도 중]");
|
||||
tft.st7735_write_str(0, 12, "Connecting WiFi...", Font_7x10, ST7735_WHITE, ST7735_BLACK);
|
||||
connectToBestWiFi();
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.print("[WiFi 연결됨]: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
tft.st7735_write_str(0, 24, "WiFi OK", Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
} else {
|
||||
tft.st7735_write_str(0, 24, "WiFi Fail", Font_7x10, ST7735_RED, ST7735_BLACK);
|
||||
}
|
||||
|
||||
Serial.println("[MQTT 연결 시도 중]");
|
||||
tft.st7735_write_str(0, 36, "Connecting MQTT...", Font_7x10, ST7735_WHITE, ST7735_BLACK);
|
||||
connectToMQTT();
|
||||
if (mqttClient.connected()) {
|
||||
Serial.println("[MQTT 연결됨]");
|
||||
tft.st7735_write_str(0, 48, "MQTT OK", Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
} else {
|
||||
Serial.println("[MQTT 연결 실패]");
|
||||
tft.st7735_write_str(0, 48, "MQTT Fail", Font_7x10, ST7735_RED, ST7735_BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void loop() {
|
||||
handleSerialCommand();
|
||||
if (!configMode) {
|
||||
ensureWiFiAndMQTTConnected();
|
||||
handleGPSAndSelfJson();
|
||||
handleLoRaReceive();
|
||||
handleMQTTPublish();
|
||||
handleTFTDisplay();
|
||||
}
|
||||
}
|
||||
253
src/sender/main.cpp
Normal file
253
src/sender/main.cpp
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
프로젝트명: GNSS + LoRa + MQTT 실시간 위치 및 환경 데이터 송신 시스템
|
||||
보드 종류: ESP32 (예: HELTEC Wireless Tracker 또는 유사 보드)
|
||||
REV 2.0
|
||||
|
||||
주요 기능:
|
||||
- GNSS(GPS) 모듈을 통해 시간, 위성, 위치, 속도, 고도 정보 수집
|
||||
- SGP30 공기질 센서로 TVOC 및 eCO2 농도 측정
|
||||
- DFRobot H2S 및 NH3 가스 센서 데이터 측정
|
||||
- OLED TFT 디스플레이에 실시간 환경 및 GPS 정보 출력
|
||||
- LoRa SX1262 모듈을 통해 JSON 형식으로 환경 데이터 송신
|
||||
- 센서 및 통신 모듈 초기화, 상태 점검 및 안정성 확보
|
||||
- TFT 출력 시 고정 길이 문자열 포맷을 적용하여 잔여 문자 문제 해결
|
||||
|
||||
출력 방식 및 개선점:
|
||||
- TFT 출력은 문자열 덮어쓰기 방식을 사용하여 화면 깜박임 최소화
|
||||
- 숫자 출력 시 "%-5d" 같은 고정 길이 포맷으로 출력하여 이전 출력 잔여 문자 방지
|
||||
|
||||
LoRa 통신 설정:
|
||||
- 주파수: 923.0 MHz
|
||||
- Spreading Factor: 7
|
||||
- Bandwidth: 125 kHz
|
||||
- Coding Rate: 4/5
|
||||
- Sync Word: 0x12
|
||||
|
||||
하드웨어 연결:
|
||||
- GNSS_TX (GPS 송신): GPIO 33
|
||||
- GNSS_RX (GPS 수신): GPIO 34
|
||||
- VGNSS_CTRL (GPS 전원 제어): GPIO 3
|
||||
- LoRa SPI 핀: NSS=8, DIO1=14, RESET=12, BUSY=13
|
||||
|
||||
사용 라이브러리:
|
||||
- HT_st7735 : OLED TFT 디스플레이 제어
|
||||
- HT_TinyGPS++ : GPS 데이터 처리
|
||||
- RadioLib : SX1262 LoRa 제어
|
||||
- DFRobot_MultiGasSensor : H2S, NH3 센서 제어
|
||||
- Seeed_Arduino_SGP30 : SGP30 센서 제어
|
||||
- ArduinoJson : JSON 데이터 직렬화
|
||||
- Wire : I2C 통신
|
||||
|
||||
개발 및 테스트 환경:
|
||||
- Arduino IDE 및 ESP32 환경
|
||||
- 센서 초기 안정화 시간 150초 설정
|
||||
- 시리얼 모니터를 통한 상태 및 디버깅 출력 지원
|
||||
|
||||
향후 개선 가능 사항:
|
||||
- TFT 부분 클리어 기능 추가 또는 라이브러리 변경으로 깜박임 개선
|
||||
- LoRa 통신 오류 감지 및 재전송 로직 강화
|
||||
- 데이터 송수신 간 동기화 및 신뢰성 강화
|
||||
|
||||
작성일: 2025-07-29
|
||||
작성자: BK Choi
|
||||
*/
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "HT_st7735.h"
|
||||
#include "HT_TinyGPS++.h"
|
||||
#include <RadioLib.h>
|
||||
#include <Wire.h>
|
||||
#include <EEPROM.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "DFRobot_MultiGasSensor.h"
|
||||
#include "sensirion_common.h"
|
||||
#include "sgp30.h"
|
||||
|
||||
#define SDA_PIN 5
|
||||
#define SCL_PIN 4
|
||||
#define GNSS_TX 33
|
||||
#define GNSS_RX 34
|
||||
#define VGNSS_CTRL 3
|
||||
|
||||
#define LORA_FREQ 923.0
|
||||
#define LORA_SPREADING_FACTOR 7
|
||||
#define LORA_BW 125.0
|
||||
#define LORA_CR 5
|
||||
#define LORA_SYNCWORD 0x12
|
||||
|
||||
HT_st7735 tft;
|
||||
TinyGPSPlus GPS;
|
||||
SX1262 radio = new Module(8, 14, 12, 13);
|
||||
DFRobot_GAS_I2C h2s(&Wire, 0x74);
|
||||
DFRobot_GAS_I2C nh3(&Wire, 0x75);
|
||||
|
||||
unsigned long lastLoRaTime = 0;
|
||||
const unsigned long loraInterval = 1000;
|
||||
|
||||
uint16_t tvoc_ppb = 0, co2_eq_ppm = 0;
|
||||
float h2s_val = 0, nh3_val = 0;
|
||||
|
||||
void setupTFT() {
|
||||
tft.st7735_init();
|
||||
tft.st7735_fill_screen(ST7735_BLACK);
|
||||
tft.st7735_write_str(0, 0, "Initializing...", Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
}
|
||||
|
||||
void setupGPS() {
|
||||
Serial1.begin(115200, SERIAL_8N1, GNSS_TX, GNSS_RX);
|
||||
}
|
||||
|
||||
void setupGasSensors() {
|
||||
while (!h2s.begin()) {
|
||||
Serial.println("No H2S sensor found");
|
||||
delay(1000);
|
||||
}
|
||||
while (!nh3.begin()) {
|
||||
Serial.println("No NH3 sensor found");
|
||||
delay(1000);
|
||||
}
|
||||
h2s.changeAcquireMode(h2s.INITIATIVE);
|
||||
nh3.changeAcquireMode(nh3.INITIATIVE);
|
||||
|
||||
while (sgp_probe() != STATUS_OK) {
|
||||
Serial.println("SGP30 failed to start");
|
||||
delay(1000);
|
||||
}
|
||||
sgp_iaq_init();
|
||||
sgp_set_absolute_humidity(1030);
|
||||
|
||||
for (int sec = 150; sec >= 0; sec--) {
|
||||
char buf[32];
|
||||
sprintf(buf, "Stabilize %d sec", sec);
|
||||
tft.st7735_write_str(0, 12, buf, Font_7x10, ST7735_YELLOW, ST7735_BLACK);
|
||||
delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void setupLoRa() {
|
||||
if (radio.begin(LORA_FREQ) == RADIOLIB_ERR_NONE) {
|
||||
radio.setSpreadingFactor(LORA_SPREADING_FACTOR);
|
||||
radio.setBandwidth(LORA_BW);
|
||||
radio.setCodingRate(LORA_CR);
|
||||
radio.setSyncWord(LORA_SYNCWORD);
|
||||
Serial.println("LoRa initialized successfully.");
|
||||
} else {
|
||||
tft.st7735_write_str(0, 0, "LoRa Fail", Font_7x10, ST7735_RED, ST7735_BLACK);
|
||||
Serial.println("LoRa initialization failed!");
|
||||
while (true);
|
||||
}
|
||||
tft.st7735_fill_screen(ST7735_BLACK);
|
||||
}
|
||||
|
||||
void updateSensors() {
|
||||
while (Serial1.available()) GPS.encode(Serial1.read());
|
||||
|
||||
uint16_t tvoc_tmp = 0, co2_tmp = 0;
|
||||
s16 ret = sgp_measure_iaq_blocking_read(&tvoc_tmp, &co2_tmp);
|
||||
if (ret == STATUS_OK) {
|
||||
tvoc_ppb = tvoc_tmp;
|
||||
co2_eq_ppm = co2_tmp;
|
||||
}
|
||||
|
||||
h2s_val = nh3_val = 0;
|
||||
if (h2s.dataIsAvailable()) h2s_val = AllDataAnalysis.gasconcentration;
|
||||
if (nh3.dataIsAvailable()) nh3_val = AllDataAnalysis.gasconcentration;
|
||||
}
|
||||
|
||||
void updateDisplay() {
|
||||
char buf[64];
|
||||
|
||||
sprintf(buf, "NODE-001 %02d:%02d:%02d",
|
||||
(GPS.time.hour() + 9) % 24,
|
||||
GPS.time.minute(),
|
||||
GPS.time.second());
|
||||
tft.st7735_write_str(0, 0, buf, Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
|
||||
sprintf(buf, "SAT:%d HDOP:%.1f",
|
||||
GPS.satellites.value(),
|
||||
GPS.hdop.hdop());
|
||||
tft.st7735_write_str(0, 12, buf, Font_7x10, ST7735_GREEN, ST7735_BLACK);
|
||||
|
||||
sprintf(buf, "LAT:%.4f LON:%.4f",
|
||||
GPS.location.lat(),
|
||||
GPS.location.lng());
|
||||
tft.st7735_write_str(0, 24, buf, Font_7x10, ST7735_YELLOW, ST7735_BLACK);
|
||||
|
||||
char spdStr[8], altStr[8];
|
||||
dtostrf(GPS.speed.kmph(), 5, 1, spdStr);
|
||||
dtostrf(GPS.altitude.meters(), 5, 1, altStr);
|
||||
sprintf(buf, "SPD:%s ALT:%s", spdStr, altStr);
|
||||
tft.st7735_write_str(0, 36, buf, Font_7x10, ST7735_CYAN, ST7735_BLACK);
|
||||
|
||||
dtostrf(nh3_val, 4, 1, spdStr);
|
||||
dtostrf(h2s_val, 4, 1, altStr);
|
||||
sprintf(buf, "NH3:%s H2S:%s", spdStr, altStr);
|
||||
tft.st7735_write_str(0, 48, buf, Font_7x10, ST7735_MAGENTA, ST7735_BLACK);
|
||||
|
||||
int tvoc_val = (tvoc_ppb <= 60000) ? tvoc_ppb : 0;
|
||||
int eco2_val = (co2_eq_ppm >= 400 && co2_eq_ppm <= 10000) ? co2_eq_ppm : 400;
|
||||
|
||||
// 고정 길이 문자열 출력 (깜박임 없이 덮어쓰기)
|
||||
sprintf(buf, "TVOC:%-5d eCO2:%-5d", tvoc_val, eco2_val);
|
||||
tft.st7735_write_str(0, 60, buf, Font_7x10, ST7735_RED, ST7735_BLACK);
|
||||
}
|
||||
|
||||
void sendLoRaData() {
|
||||
unsigned long now = millis();
|
||||
if (now - lastLoRaTime >= loraInterval) {
|
||||
lastLoRaTime = now;
|
||||
|
||||
StaticJsonDocument<256> doc;
|
||||
char timeStr[10];
|
||||
sprintf(timeStr, "%02d:%02d:%02d",
|
||||
(GPS.time.hour() + 9) % 24,
|
||||
GPS.time.minute(),
|
||||
GPS.time.second());
|
||||
|
||||
doc["id"] = "NODE-001";
|
||||
doc["time"] = timeStr;
|
||||
doc["lat"] = GPS.location.lat();
|
||||
doc["lon"] = GPS.location.lng();
|
||||
doc["alt"] = GPS.altitude.meters();
|
||||
doc["spd"] = GPS.speed.kmph();
|
||||
doc["sat"] = GPS.satellites.value();
|
||||
doc["hdop"] = GPS.hdop.hdop();
|
||||
doc["nh3"] = nh3_val;
|
||||
doc["h2s"] = h2s_val;
|
||||
|
||||
int tvoc_val = (tvoc_ppb <= 60000) ? tvoc_ppb : 0;
|
||||
int eco2_val = (co2_eq_ppm >= 400 && co2_eq_ppm <= 10000) ? co2_eq_ppm : 400;
|
||||
|
||||
doc["tvoc"] = tvoc_val;
|
||||
doc["eco2"] = eco2_val;
|
||||
|
||||
char buffer[256];
|
||||
serializeJson(doc, buffer);
|
||||
|
||||
if (radio.transmit(buffer) == RADIOLIB_ERR_NONE) {
|
||||
Serial.print("LoRa Sent: ");
|
||||
Serial.println(buffer);
|
||||
} else {
|
||||
Serial.println("LoRa transmission failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(SDA_PIN, SCL_PIN);
|
||||
|
||||
pinMode(VGNSS_CTRL, OUTPUT);
|
||||
digitalWrite(VGNSS_CTRL, HIGH);
|
||||
|
||||
setupTFT();
|
||||
setupGPS();
|
||||
setupGasSensors();
|
||||
setupLoRa();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
updateSensors();
|
||||
updateDisplay();
|
||||
sendLoRaData();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user