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