Initial commit: add environment monitoring TCP backend

This commit is contained in:
yzfh
2026-10-19 15:52:36 +08:00
commit c892ba07f4
5 changed files with 2952 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
/bin/
/data/
/logs/
*.log
*.o
+23
View File
@@ -0,0 +1,23 @@
CC = g++
CXXFLAGS = -std=c++17 -O2 -Wall -pthread
SRCDIR = src
BINDIR = bin
TARGETS = $(BINDIR)/device_backend $(BINDIR)/console_client
SRC_BACKEND = $(SRCDIR)/server.cpp
SRC_CLIENT = console_client.cpp
all: $(TARGETS)
$(BINDIR):
mkdir -p $(BINDIR)
$(BINDIR)/device_backend: $(SRC_BACKEND) | $(BINDIR)
$(CC) $(CXXFLAGS) -o $(BINDIR)/device_backend $(SRC_BACKEND)
$(BINDIR)/console_client: $(SRC_CLIENT) | $(BINDIR)
$(CC) $(CXXFLAGS) -o $(BINDIR)/console_client $(SRC_CLIENT)
clean:
rm -f $(TARGETS) backend.log
.PHONY: all clean
+2379
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
int main(int argc, char **argv) {
if (argc < 3) {
std::cout << "Usage: " << argv[0] << " <host> <port>\n";
return 1;
}
const char *host = argv[1];
int port = std::atoi(argv[2]);
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
struct sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
perror("inet_pton");
close(sock);
return 1;
}
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect");
close(sock);
return 1;
}
std::cout << "Connected to " << host << ":" << port << std::endl;
char buf[1024];
while (true) {
ssize_t n = recv(sock, buf, sizeof(buf)-1, 0);
if (n > 0) {
buf[n] = '\0';
std::cout << buf;
std::cout.flush();
} else if (n == 0) {
std::cout << "Server closed connection\n";
break;
} else {
perror("recv");
break;
}
}
close(sock);
return 0;
}
+489
View File
@@ -0,0 +1,489 @@
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <atomic>
#include <cstring>
#include <fstream>
#include <iostream>
#include <sys/stat.h>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <algorithm>
#include <signal.h>
#include <map>
#include <chrono>
#include <ctime>
#include <iomanip>
static std::mutex log_mutex;
static std::atomic<bool> g_running{true};
static std::vector<int> g_clients;
static std::mutex g_clients_mutex;
// Device connection management
struct DeviceInfo {
std::string device_id;
std::string last_seen;
std::string status; // "connected", "disconnected"
int client_fd;
};
static std::map<std::string, DeviceInfo> g_devices;
static std::mutex g_devices_mutex;
void write_log(const std::string &line)
{
std::lock_guard<std::mutex> lk(log_mutex);
std::ofstream ofs("backend.log", std::ios::app);
if (ofs) {
ofs << line << std::endl;
}
}
// data directory for per-device files
static const char *DATA_DIR = "data";
static std::mutex file_mutex;
static void ensure_data_dir()
{
struct stat st;
if (stat(DATA_DIR, &st) != 0) {
// try to create
mkdir(DATA_DIR, 0755);
}
}
// write content to data/<device>_<type>.json (type = sensor|control)
static void write_device_file(const std::string &device_id, const std::string &type, const std::string &content)
{
std::lock_guard<std::mutex> lk(file_mutex);
ensure_data_dir();
std::string fname = std::string(DATA_DIR) + "/" + device_id + "_" + type + ".json";
std::ofstream ofs(fname, std::ios::trunc);
if (ofs) {
ofs << content << std::endl;
}
}
// Initialize default control state for a device if not exists
static void init_default_control_state(const std::string &device_id)
{
std::string fname = std::string(DATA_DIR) + "/" + device_id + "_control.json";
struct stat st;
if (stat(fname.c_str(), &st) != 0) {
// File doesn't exist, create default control state
std::string default_control = "{\"device_id\":\"" + device_id + "\",\"buzzer\":0,\"led0\":0,\"led1\":0,\"led2\":0}";
write_device_file(device_id, "control", default_control);
std::cout << "Created default control state for device: " << device_id << std::endl;
}
}
// read content from data/<device>_<type>.json, return empty string if missing
static std::string read_device_file(const std::string &device_id, const std::string &type)
{
std::lock_guard<std::mutex> lk(file_mutex);
ensure_data_dir();
std::string fname = std::string(DATA_DIR) + "/" + device_id + "_" + type + ".json";
std::ifstream ifs(fname);
if (!ifs) return std::string();
std::string s;
std::string line;
while (std::getline(ifs, line)) {
if (!s.empty()) s += "\n";
s += line;
}
return s;
}
// read control state, return default if missing
static std::string read_control_state(const std::string &device_id)
{
// Ensure default control state exists
init_default_control_state(device_id);
std::string content = read_device_file(device_id, "control");
if (content.empty()) {
// If still empty, return default
return "{\"device_id\":\"" + device_id + "\",\"buzzer\":0,\"led0\":0,\"led1\":0,\"led2\":0}";
}
return content;
}
// add client fd to global list
void add_client_fd(int fd)
{
std::lock_guard<std::mutex> lk(g_clients_mutex);
g_clients.push_back(fd);
std::cout << "add_client_fd: " << fd << " (total=" << g_clients.size() << ")" << std::endl;
}
// remove client fd from global list
void remove_client_fd(int fd)
{
std::lock_guard<std::mutex> lk(g_clients_mutex);
auto it = std::find(g_clients.begin(), g_clients.end(), fd);
if (it != g_clients.end()) g_clients.erase(it);
std::cout << "remove_client_fd: " << fd << " (total=" << g_clients.size() << ")" << std::endl;
}
// broadcast a line to all connected clients except exclude_fd (if exclude_fd==-1 send to all)
// Forward declaration
void broadcast_line(const std::string &line, int exclude_fd);
// Get current timestamp string
std::string get_timestamp()
{
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
return ss.str();
}
// Register or update device connection
void register_device(const std::string &device_id, int client_fd, const std::string &action)
{
std::lock_guard<std::mutex> lk(g_devices_mutex);
DeviceInfo &info = g_devices[device_id];
info.device_id = device_id;
info.last_seen = get_timestamp();
info.client_fd = client_fd;
if (action == "connect") {
info.status = "connected";
std::cout << "Device registered: " << device_id << " (fd=" << client_fd << ")" << std::endl;
// Initialize default control state for new device
init_default_control_state(device_id);
} else if (action == "reconnect") {
info.status = "connected";
std::cout << "Device reconnected: " << device_id << " (fd=" << client_fd << ")" << std::endl;
}
// Broadcast device connection status to UI clients
std::string status_msg = "{\"type\":\"device_status\",\"device_id\":\"" + device_id +
"\",\"status\":\"" + info.status + "\",\"timestamp\":\"" + info.last_seen + "\"}";
broadcast_line(status_msg, client_fd);
}
// Mark device as disconnected
void unregister_device(int client_fd)
{
std::lock_guard<std::mutex> lk(g_devices_mutex);
for (auto &pair : g_devices) {
if (pair.second.client_fd == client_fd && pair.second.status == "connected") {
pair.second.status = "disconnected";
pair.second.last_seen = get_timestamp();
std::string device_id = pair.first;
std::cout << "Device disconnected: " << device_id << " (fd=" << client_fd << ")" << std::endl;
// Broadcast device disconnection status to UI clients
std::string status_msg = "{\"type\":\"device_status\",\"device_id\":\"" + device_id +
"\",\"status\":\"disconnected\",\"timestamp\":\"" + pair.second.last_seen + "\"}";
broadcast_line(status_msg, client_fd);
break;
}
}
}
// Get list of all devices
std::string get_devices_list()
{
std::lock_guard<std::mutex> lk(g_devices_mutex);
std::string result = "{\"type\":\"devices_list\",\"devices\":[";
bool first = true;
for (const auto &pair : g_devices) {
if (!first) result += ",";
result += "{\"device_id\":\"" + pair.second.device_id +
"\",\"status\":\"" + pair.second.status +
"\",\"last_seen\":\"" + pair.second.last_seen + "\"}";
first = false;
}
result += "]}";
return result;
}
void broadcast_line(const std::string &line, int exclude_fd)
{
std::lock_guard<std::mutex> lk(g_clients_mutex);
std::string out = line + "\n";
for (int fd : g_clients) {
if (fd == exclude_fd) continue;
ssize_t sent = send(fd, out.c_str(), (ssize_t)out.size(), MSG_NOSIGNAL);
if (sent <= 0) {
int err = errno;
std::cout << "broadcast_line: send to fd=" << fd << " failed, sent=" << sent << " errno=" << err << "\n";
} else {
std::cout << "broadcast_line: sent to fd=" << fd << " bytes=" << sent << "\n";
}
}
}
void handle_client(int client_fd, std::string peer)
{
std::cout << "Client connected: " << peer << std::endl;
// client already added in accept loop
std::string buffer;
char tmp[512];
while (g_running) {
ssize_t n = recv(client_fd, tmp, sizeof(tmp), 0);
if (n > 0) {
buffer.append(tmp, tmp + n);
// process lines
size_t pos = 0;
while ((pos = buffer.find('\n')) != std::string::npos) {
std::string line = buffer.substr(0, pos);
// trim \r
if (!line.empty() && line.back() == '\r')
line.pop_back();
if (!line.empty()) {
std::cout << "RECV: " << line << std::endl;
write_log(line);
// If this is a GET request: "GET sensor <device>" or "GET control <device>" or "GET devices"
if (line.rfind("GET ", 0) == 0) {
// tokenize
std::string cmd = line.substr(4);
size_t sp = cmd.find(' ');
if (sp != std::string::npos) {
std::string type = cmd.substr(0, sp);
if (type == "devices") {
// Send devices list
std::string content = get_devices_list();
std::string out = content + "\n";
ssize_t sent = send(client_fd, out.c_str(), (ssize_t)out.size(), MSG_NOSIGNAL);
if (sent <= 0) std::cout << "GET devices: send to fd=" << client_fd << " failed\n";
} else {
std::string device = cmd.substr(sp + 1);
if (!device.empty()) {
std::string content;
if (type == "control") {
// For control requests, always return valid control state
content = read_control_state(device);
} else {
// For other types (sensor), read from file
content = read_device_file(device, type);
if (content.empty()) content = "{}";
}
std::string out = content + "\n";
ssize_t sent = send(client_fd, out.c_str(), (ssize_t)out.size(), MSG_NOSIGNAL);
if (sent <= 0) std::cout << "GET " << type << " for " << device << ": send to fd=" << client_fd << " failed\n";
else std::cout << "GET " << type << " for " << device << ": sent " << sent << " bytes to fd=" << client_fd << "\n";
}
}
} else {
// Single word command like "GET devices"
if (cmd == "devices") {
// Send devices list
std::string content = get_devices_list();
std::string out = content + "\n";
ssize_t sent = send(client_fd, out.c_str(), (ssize_t)out.size(), MSG_NOSIGNAL);
if (sent <= 0) std::cout << "GET devices: send to fd=" << client_fd << " failed\n";
else std::cout << "GET devices: sent " << sent << " bytes to fd=" << client_fd << "\n";
}
}
} else {
// For JSON messages: detect registration, sensor data, or control data
bool is_json = (!line.empty() && (line.front() == '{' || line.front() == '['));
if (is_json) {
// Check if this is a registration message
if (line.find("\"type\":\"register\"") != std::string::npos) {
// Extract device_id and action
std::string device_id = "unknown";
std::string action = "connect";
size_t p = line.find("\"device_id\"");
if (p != std::string::npos) {
p = line.find(':', p);
if (p != std::string::npos) {
size_t q = line.find('"', p);
if (q != std::string::npos) {
size_t r = line.find('"', q + 1);
if (r != std::string::npos) device_id = line.substr(q + 1, r - q - 1);
}
}
}
p = line.find("\"action\"");
if (p != std::string::npos) {
p = line.find(':', p);
if (p != std::string::npos) {
size_t q = line.find('"', p);
if (q != std::string::npos) {
size_t r = line.find('"', q + 1);
if (r != std::string::npos) action = line.substr(q + 1, r - q - 1);
}
}
}
register_device(device_id, client_fd, action);
} else {
// Regular sensor or control data
bool is_sensor = (line.find("\"temp\"") != std::string::npos) || (line.find("\"humidity\"") != std::string::npos);
bool is_control = (line.find("\"buzzer\"") != std::string::npos) || (line.find("\"led0\"") != std::string::npos) ||
(line.find("\"led1\"") != std::string::npos) || (line.find("\"led2\"") != std::string::npos);
// extract device_id if present
std::string device_id = "unknown";
size_t p = line.find("\"device_id\"");
if (p != std::string::npos) {
p = line.find(':', p);
if (p != std::string::npos) {
size_t q = line.find('"', p);
if (q != std::string::npos) {
size_t r = line.find('"', q + 1);
if (r != std::string::npos) device_id = line.substr(q + 1, r - q - 1);
}
}
}
// Update last_seen time for connected devices
{
std::lock_guard<std::mutex> lk(g_devices_mutex);
auto it = g_devices.find(device_id);
if (it != g_devices.end() && it->second.status == "connected") {
it->second.last_seen = get_timestamp();
}
}
// Separate handling for sensor and control data
if (is_sensor && is_control) {
// Message contains both sensor and control data (from device)
// Store sensor data ONLY, ignore control states from sensor messages
std::cout << "Received combined sensor+control from device " << device_id << ", storing sensor data only" << std::endl;
// Create clean sensor data with only device_id, temp, and humidity
std::string clean_sensor_data = "{\"device_id\":\"" + device_id + "\"";
// Extract temp if present
size_t temp_pos = line.find("\"temp\":");
if (temp_pos != std::string::npos) {
size_t colon_pos = line.find(':', temp_pos);
size_t comma_pos = line.find(',', colon_pos);
if (comma_pos == std::string::npos) comma_pos = line.find('}', colon_pos);
if (comma_pos != std::string::npos) {
std::string temp_value = line.substr(colon_pos + 1, comma_pos - colon_pos - 1);
clean_sensor_data += ",\"temp\":" + temp_value;
}
}
// Extract humidity if present
size_t hum_pos = line.find("\"humidity\":");
if (hum_pos != std::string::npos) {
size_t colon_pos = line.find(':', hum_pos);
size_t comma_pos = line.find(',', colon_pos);
if (comma_pos == std::string::npos) comma_pos = line.find('}', colon_pos);
if (comma_pos != std::string::npos) {
std::string hum_value = line.substr(colon_pos + 1, comma_pos - colon_pos - 1);
clean_sensor_data += ",\"humidity\":" + hum_value;
}
}
clean_sensor_data += "}";
write_device_file(device_id, "sensor", clean_sensor_data);
} else if (is_sensor) {
// Pure sensor data (without control states)
write_device_file(device_id, "sensor", line);
} else if (is_control) {
// Pure control data (from Qt interface)
write_device_file(device_id, "control", line);
std::cout << "Stored control command from Qt for device " << device_id << std::endl;
// Send control command back to the device that should execute it
std::lock_guard<std::mutex> lk(g_devices_mutex);
auto it = g_devices.find(device_id);
if (it != g_devices.end() && it->second.status == "connected") {
std::string control_msg = line + "\n";
ssize_t sent = send(it->second.client_fd, control_msg.c_str(), (ssize_t)control_msg.size(), MSG_NOSIGNAL);
if (sent > 0) {
std::cout << "Forwarded control command to device " << device_id << " (fd=" << it->second.client_fd << "), sent " << sent << " bytes" << std::endl;
} else {
std::cout << "Failed to forward control command to device " << device_id << std::endl;
}
}
}
}
}
// broadcast received line to other connected clients (e.g. UI)
broadcast_line(line, client_fd);
}
}
buffer.erase(0, pos + 1);
}
} else if (n == 0) {
// closed by peer
break;
} else {
// error
if (errno == EINTR) continue;
break;
}
}
close(client_fd);
// Mark device as disconnected
unregister_device(client_fd);
// remove from global client list
remove_client_fd(client_fd);
std::cout << "Client disconnected: " << peer << std::endl;
}
int main(int argc, char **argv)
{
// ignore SIGPIPE so sends to closed sockets do not kill the process
signal(SIGPIPE, SIG_IGN);
int port = 5000;
if (argc >= 2) port = std::atoi(argv[1]);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
return 1;
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(listen_fd);
return 1;
}
if (listen(listen_fd, 10) < 0) {
perror("listen");
close(listen_fd);
return 1;
}
std::cout << "Server listening on port " << port << std::endl;
// accept loop
while (g_running) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
if (errno == EINTR) continue;
perror("accept");
break;
}
char ipbuf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ipbuf, sizeof(ipbuf));
int client_port = ntohs(client_addr.sin_port);
std::string peer = std::string(ipbuf) + ":" + std::to_string(client_port);
// track client and spawn handler thread
add_client_fd(client_fd);
std::thread t(handle_client, client_fd, peer);
t.detach();
}
close(listen_fd);
return 0;
}