619 lines
16 KiB
C++
619 lines
16 KiB
C++
#include <WiFi.h>
|
|
#include <WebServer.h>
|
|
#include <ArduinoJson.h>
|
|
#include <SPI.h>
|
|
#include <SPIFFS.h>
|
|
#include <HardwareSerial.h>
|
|
|
|
HardwareSerial uart(0);
|
|
|
|
// WiFi credential
|
|
// Use different ssid for both module to avoid confusion
|
|
// example:
|
|
// const char* ssid = "ESP32-LoRa-Chat1"; for user 1
|
|
// const char* ssid = "ESP32-LoRa-Chat2"; for user 2
|
|
const char* ssid = "ESP32-LoRa-Chat";
|
|
const char* password = "chatpass123";
|
|
|
|
WebServer server(80);
|
|
|
|
#define MAX_MESSAGES 50
|
|
String messageBuffer[MAX_MESSAGES];
|
|
int messageCount = 0;
|
|
|
|
const char index_html[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ESP32 LoRa Chat</title>
|
|
<style>
|
|
:root {
|
|
--primary-color: #3498db;
|
|
--secondary-color: #2ecc71;
|
|
--background-gradient-1: #667eea;
|
|
--background-gradient-2: #764ba2;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background: linear-gradient(135deg, var(--background-gradient-1), var(--background-gradient-2));
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px 0;
|
|
}
|
|
|
|
#username-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
}
|
|
|
|
#username-input {
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
width: 200px;
|
|
}
|
|
|
|
#status-bar {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
padding: 10px;
|
|
border-radius: 10px;
|
|
margin-bottom: 10px;
|
|
color: white;
|
|
font-size: 0.9em;
|
|
text-align: center;
|
|
width: 90%;
|
|
max-width: 400px;
|
|
}
|
|
|
|
#chat-container {
|
|
width: 90%;
|
|
max-width: 400px;
|
|
height: 65vh;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
border-radius: 20px;
|
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#message-area {
|
|
flex-grow: 1;
|
|
overflow-y: auto;
|
|
padding: 15px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.message {
|
|
max-width: 80%;
|
|
padding: 10px 15px;
|
|
border-radius: 15px;
|
|
margin-bottom: 10px;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.sent-message {
|
|
background: linear-gradient(135deg, #3498db, #2980b9);
|
|
color: white;
|
|
align-self: flex-end;
|
|
border-bottom-right-radius: 5px;
|
|
}
|
|
|
|
.received-message {
|
|
background: linear-gradient(135deg, #ecf0f1, #bdc3c7);
|
|
color: #2c3e50;
|
|
align-self: flex-start;
|
|
border-bottom-left-radius: 5px;
|
|
}
|
|
|
|
.username {
|
|
font-weight: bold;
|
|
font-size: 0.8em;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.message-info {
|
|
font-size: 0.7em;
|
|
opacity: 0.7;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.input-area {
|
|
display: flex;
|
|
padding: 15px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
#message-input {
|
|
flex-grow: 1;
|
|
padding: 10px 15px;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: none;
|
|
border-radius: 30px;
|
|
margin-right: 10px;
|
|
color: white;
|
|
}
|
|
|
|
#message-input::placeholder {
|
|
color: rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
button {
|
|
background: linear-gradient(135deg, #3498db, #2ecc71);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 30px;
|
|
cursor: pointer;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
button:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="username-modal">
|
|
<div class="modal-content">
|
|
<h2>Enter your username</h2>
|
|
<input type="text" id="username-input" placeholder="Username">
|
|
<button id="username-submit">Join Chat</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="status-bar">
|
|
Connecting...
|
|
</div>
|
|
|
|
<div id="chat-container">
|
|
<div id="message-area"></div>
|
|
<div class="input-area">
|
|
<input type="text" id="message-input" placeholder="Type a message..." disabled>
|
|
<button id="send-btn" disabled>Send</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const statusBar = document.getElementById('status-bar');
|
|
const messageArea = document.getElementById('message-area');
|
|
const messageInput = document.getElementById('message-input');
|
|
const sendBtn = document.getElementById('send-btn');
|
|
const usernameModal = document.getElementById('username-modal');
|
|
const usernameInput = document.getElementById('username-input');
|
|
const usernameSubmit = document.getElementById('username-submit');
|
|
|
|
let username = '';
|
|
let messageCheckInterval;
|
|
|
|
usernameSubmit.addEventListener('click', () => {
|
|
username = usernameInput.value.trim();
|
|
if (username) {
|
|
usernameModal.style.display = 'none';
|
|
initChat();
|
|
}
|
|
});
|
|
|
|
function initChat() {
|
|
statusBar.textContent = 'Connected';
|
|
statusBar.style.background = 'rgba(46, 204, 113, 0.2)';
|
|
messageInput.disabled = false;
|
|
sendBtn.disabled = false;
|
|
|
|
// Start checking for new messages
|
|
messageCheckInterval = setInterval(checkNewMessages, 1000);
|
|
}
|
|
|
|
function checkNewMessages() {
|
|
fetch('/messages')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
messageArea.innerHTML = ''; // Clear existing messages
|
|
data.messages.forEach(msg => {
|
|
const [sender, content] = msg.split(': ');
|
|
const type = sender === username ? 'sent' : 'received';
|
|
addMessage(content, type, sender);
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching messages:', error);
|
|
statusBar.textContent = 'Connection error';
|
|
statusBar.style.background = 'rgba(231, 76, 60, 0.2)';
|
|
});
|
|
}
|
|
|
|
function addMessage(content, type, sender) {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.classList.add('message', `${type}-message`);
|
|
|
|
const usernameDiv = document.createElement('div');
|
|
usernameDiv.classList.add('username');
|
|
usernameDiv.textContent = sender;
|
|
messageDiv.appendChild(usernameDiv);
|
|
|
|
const messageContent = document.createElement('div');
|
|
messageContent.textContent = content;
|
|
messageDiv.appendChild(messageContent);
|
|
|
|
const messageInfo = document.createElement('div');
|
|
messageInfo.classList.add('message-info');
|
|
messageInfo.textContent = new Date().toLocaleTimeString();
|
|
messageDiv.appendChild(messageInfo);
|
|
|
|
messageArea.appendChild(messageDiv);
|
|
messageArea.scrollTop = messageArea.scrollHeight;
|
|
}
|
|
|
|
function sendMessage() {
|
|
const message = messageInput.value.trim();
|
|
if (message) {
|
|
fetch('/send', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
message: message,
|
|
sender: username
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'sent') {
|
|
messageInput.value = '';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error sending message:', error);
|
|
statusBar.textContent = 'Error sending message';
|
|
statusBar.style.background = 'rgba(231, 76, 60, 0.2)';
|
|
});
|
|
}
|
|
}
|
|
|
|
sendBtn.addEventListener('click', sendMessage);
|
|
messageInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
sendMessage();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
)rawliteral";
|
|
|
|
void LoRa_Join(){
|
|
uint32_t time = millis();
|
|
bool msg_done = true;
|
|
|
|
uart.println("AT+MODE=LWOTAA");
|
|
delay(1000);
|
|
|
|
while(1){
|
|
if(msg_done){
|
|
uart.println("AT+JOIN");
|
|
msg_done = false;
|
|
}
|
|
if(uart.available()){
|
|
String str = uart.readStringUntil('\n');
|
|
Serial.println(str);
|
|
|
|
if(strcmp(str.c_str(), "+JOIN: Join failed\r") == 0){
|
|
msg_done = true;
|
|
}
|
|
if(strcmp(str.c_str(), "+JOIN: Done\r") == 0){
|
|
if(!msg_done){
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LoRa_Send_MSG(char* msg){
|
|
uart.println(msg);
|
|
while(1){
|
|
if(uart.available()){
|
|
String str = uart.readStringUntil('\n');
|
|
Serial.println(str);
|
|
if(strcmp(str.c_str(), "+MSG: Done\r") == 0){
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
uint8_t lora_msg[32][51];
|
|
|
|
void LoRa_Send_MSGHEX(const char* msg){//parses string into multiple messages if needed
|
|
//clear send buffer
|
|
for(int i = 0; i < 32; i++){
|
|
for(int j = 0; j < 51; j++){
|
|
lora_msg[i][j] = 0;
|
|
}
|
|
}
|
|
//get how many messages we will need (32 max = 1536 characters)
|
|
int len = strlen(msg);
|
|
int msg_no = len / 48;
|
|
if((len % 48) != 0){
|
|
msg_no ++;
|
|
}
|
|
//prepare messages
|
|
for(int i = 0; i < msg_no; i++){
|
|
if(len > 48){
|
|
lora_msg[i][0] = 51;
|
|
lora_msg[i][1] = i+1;
|
|
lora_msg[i][2] = msg_no;
|
|
for(int j = 0; j < 48; j++){
|
|
lora_msg[i][3+j] = msg[j+i*48];
|
|
}
|
|
len -= 48;
|
|
}
|
|
else{
|
|
lora_msg[i][0] = len+3;
|
|
lora_msg[i][1] = i+1;
|
|
lora_msg[i][2] = msg_no;
|
|
for(int j = 0; j < len; j++){
|
|
lora_msg[i][3+j] = msg[j+i*48];
|
|
}
|
|
len = 0;
|
|
}
|
|
}
|
|
//send messages
|
|
for(int i = 0; i < 32; i++){
|
|
if(lora_msg[i][0] != 0){
|
|
uart.print("AT+MSGHEX=\"");
|
|
Serial.print("AT+MSGHEX=\"");
|
|
for(int j = 0; j < lora_msg[i][0]; j++){
|
|
uart.printf("%02X",lora_msg[i][j]);
|
|
Serial.printf("%02X",lora_msg[i][j]);
|
|
}
|
|
uart.println("\"");
|
|
Serial.println("\"");
|
|
}
|
|
else{
|
|
return;
|
|
}
|
|
|
|
while(1){
|
|
if(uart.available()){
|
|
String str = uart.readStringUntil('\n');
|
|
Serial.println(str);
|
|
if(strcmp(str.c_str(), "+MSGHEX: Done\r") == 0){
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int asciiToHex(char *asciiHexChar){
|
|
uint8_t hi, lo;
|
|
uint8_t x;
|
|
|
|
x = (asciiHexChar[0] & 0x4F);
|
|
hi = (x & 0x0F) + ((x >> 6) * 9);
|
|
x = (asciiHexChar[1] & 0x4F);
|
|
lo = (x & 0x0F) + ((x >> 6) * 9);
|
|
|
|
return (hi << 4) + lo;
|
|
}
|
|
|
|
|
|
int rx_buffer[32][51];
|
|
int rx_index = 0;
|
|
|
|
void LoRa_Receive_MSG(){
|
|
if(uart.available()){
|
|
String str = uart.readStringUntil('\n');
|
|
Serial.println(str);
|
|
if(str.indexOf("MSG: PORT: 1; RX:") > 0){
|
|
char rxBytes[3] = {0,0,0};
|
|
int rx_len = 51;
|
|
for(int i = 0; i < rx_len; i++){
|
|
for(int j = 0; j < 2; j++){
|
|
rxBytes[j] = str.c_str()[i*2+j+20];
|
|
}
|
|
rx_buffer[rx_index][i] = asciiToHex(rxBytes);
|
|
if(i == 0){
|
|
rx_len = rx_buffer[rx_index][0];
|
|
}
|
|
}
|
|
if(rx_buffer[rx_index][1] == rx_buffer[rx_index][2]){
|
|
//transmit whole message
|
|
rx_index = 0;
|
|
String rxString = "";
|
|
|
|
for(int i = 0; i < 32; i++){
|
|
for(int j = 3; j < (rx_buffer[i][0]); j++){
|
|
rxString += (char)rx_buffer[i][j];
|
|
}
|
|
if(rx_buffer[i][0] < 51){
|
|
break;
|
|
}
|
|
}
|
|
addMessageToBuffer(rxString);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
uart.begin(9600, SERIAL_8N1, RX, TX);
|
|
|
|
if(!SPIFFS.begin(true)) {
|
|
Serial.println("SPIFFS initialization failed!");
|
|
return;
|
|
}
|
|
|
|
Serial.println("Initializing LoRa...");
|
|
//add LoRa E5 module init here
|
|
pinMode(5, OUTPUT);
|
|
digitalWrite(5, LOW);
|
|
delay(1000);
|
|
digitalWrite(5, HIGH);
|
|
delay(2000);
|
|
|
|
LoRa_Join();
|
|
LoRa_Send_MSG("AT+MSG=\"hello\"");
|
|
delay(1000);
|
|
uart.println("AT+CLASS=C");
|
|
//LoRa E5 module init done
|
|
|
|
WiFi.softAP(ssid, password);
|
|
Serial.println("Access Point Started");
|
|
Serial.print("IP Address: ");
|
|
Serial.println(WiFi.softAPIP());
|
|
|
|
server.on("/", HTTP_GET, handleRoot);
|
|
server.on("/send", HTTP_POST, handleSendMessage);
|
|
server.on("/messages", HTTP_GET, handleGetMessages);
|
|
|
|
server.begin();
|
|
Serial.println("Web Server Started");
|
|
}
|
|
|
|
void handleRoot() {
|
|
server.send(200, "text/html", index_html);
|
|
}
|
|
|
|
void handleSendMessage() {
|
|
if (server.hasArg("plain")) {
|
|
StaticJsonDocument<200> doc;
|
|
DeserializationError error = deserializeJson(doc, server.arg("plain"));
|
|
|
|
if (!error) {
|
|
String message = doc["message"].as<String>();
|
|
String sender = doc["sender"].as<String>();
|
|
|
|
Serial.println("Sending message");
|
|
Serial.print("Sender: ");
|
|
Serial.println(sender);
|
|
Serial.print("Message: ");
|
|
Serial.println(message);
|
|
|
|
String lora_msg = sender + ": " + message;
|
|
LoRa_Send_MSGHEX(lora_msg.c_str());
|
|
|
|
addMessageToBuffer(sender + ": " + message);
|
|
|
|
server.send(200, "application/json", "{\"status\":\"sent\"}");
|
|
} else {
|
|
server.send(400, "application/json", "{\"error\":\"Invalid JSON\"}");
|
|
}
|
|
}
|
|
}
|
|
|
|
void handleGetMessages() {
|
|
StaticJsonDocument<2048> doc;
|
|
JsonArray array = doc.createNestedArray("messages");
|
|
|
|
for(int i = 0; i < messageCount; i++) {
|
|
array.add(messageBuffer[i]);
|
|
}
|
|
|
|
String response;
|
|
serializeJson(doc, response);
|
|
server.send(200, "application/json", response);
|
|
}
|
|
|
|
void addMessageToBuffer(String message) {
|
|
if (messageCount >= MAX_MESSAGES) {
|
|
for(int i = 0; i < MAX_MESSAGES - 1; i++) {
|
|
messageBuffer[i] = messageBuffer[i + 1];
|
|
}
|
|
messageCount--;
|
|
}
|
|
messageBuffer[messageCount++] = message;
|
|
}
|
|
|
|
String incoming = "";
|
|
int incomingByte = 0;
|
|
|
|
void loop() {
|
|
server.handleClient();
|
|
|
|
LoRa_Receive_MSG();
|
|
|
|
//if(uart.available()){
|
|
// String str = uart.readStringUntil('\n');
|
|
// Serial.println(str);
|
|
// if(str.indexOf("MSG: PORT: 1; RX:") > 0){
|
|
// Serial.println("yayy");
|
|
// }
|
|
//}
|
|
//delay(1000);
|
|
|
|
//if(Serial.available()){
|
|
// String str = Serial.readStringUntil('\n');
|
|
// uart.println(str);
|
|
//}
|
|
/*
|
|
if (Serial.available() > 0) {
|
|
incomingByte = Serial.read();
|
|
|
|
if(incomingByte != '#'){
|
|
incoming += (char)incomingByte;
|
|
}
|
|
|
|
if(incomingByte == '#'){
|
|
addMessageToBuffer(incoming);
|
|
incoming = "";
|
|
}
|
|
}
|
|
*/
|
|
/*
|
|
int packetSize = LoRa.parsePacket();
|
|
if (packetSize) {
|
|
byte recipient = LoRa.read();
|
|
byte sender = LoRa.read();
|
|
|
|
String incoming = "";
|
|
while (LoRa.available()) {
|
|
incoming += (char)LoRa.read();
|
|
}
|
|
|
|
if (recipient == localAddress) {
|
|
addMessageToBuffer(incoming);
|
|
}
|
|
}*/
|
|
}
|