Sensor Monitoring System

·

14 min read

Sensor Monitoring System

INTRODUCTION

  • Cũng lâu rồi tôi không ra bài gì về mảng Embedded vì có nhiều việc bận quá. Nhưng hôm nay nhã hứng nên tôi muốn viết về một project cá nhân tôi mới làm về Linux Programming. Đó là hệ thống giám sát cảm biến. Nói chung đó là 1 project khá cơ bản về Linux cho những người mới bắt đầu( như tôi) có thể thực hiện được!

OVERVIEW

  • Hệ thống giám sát cảm biến bao gồm các nút cảm biến đo nhiệt độ phòng,cổng cảm biến thu thập tất cả dữ liệu cảm biến từ các nút cảm biến và cơ sở dữ liệu SQL để lưu trữ tất cả dữ liệu cảm biến được xử lý bởi cổng cảm biến. Nút cảm biến sử dụng TCP riêng kết nối để truyền dữ liệu cảm biến đến cổng cảm biến. Cơ sở dữ liệu SQL là SQLite. Hệ thống đầy đủ được mô tả dưới đây.

  • Cổng cảm biến có thể không đảm nhận số lượng cảm biến tối đa khi khởi động. Trên thực tế, số lượng cảm biến kết nối với cổng cảm biến không cố định và có thể thay đổi theo thời gian. Làm việc với các nút cảm biến nhúng thực không phải là một lựa chọn cho nhiệm vụ này. Vì thế, các nút cảm biến sẽ được mô phỏng trong phần mềm

Sensor Gateway (Cổng cảm biến)

  • Thiết kế chi tiết hơn của cổng cảm biến được mô tả bên dưới. Trong phần sau, chúng tôi sẽ thảo luận chi tiết hơn về các yêu cầu tối thiểu của cả hai quy trình.

MINIMUM REQUIREMENTS

## Yêu cầu 1

  • Cổng cảm biến bao gồm một tiến trình chính và một tiến trình ghi log. Tiến trình ghi log được khởi động (bằng fork) như một tiến trình con của tiến trình chính.

## Yêu cầu 2

  • Tiến trình chính chạy ba luồng: luồng quản lý kết nối, luồng quản lý dữ liệu và luồng quản lý lưu trữ. Một cấu trúc dữ liệu chia sẻ được sử dụng để giao tiếp giữa tất cả các luồng. Lưu ý rằng việc truy cập đọc/ghi/cập nhật vào dữ liệu chia sẻ cần phải an toàn cho luồng!

## Yêu cầu 3

  • Trình quản lý kết nối lắng nghe trên một socket TCP để nhận các yêu cầu kết nối đến từ các nút cảm biến mới. Số cổng của kết nối TCP này được cung cấp như một đối số dòng lệnh khi khởi động tiến trình chính, ví dụ: ./server 1234

## Yêu cầu 4

  • Trình quản lý kết nối bắt các gói tin đến từ các nút cảm biến như được định nghĩa trong . Tiếp theo, trình quản lý kết nối ghi dữ liệu vào cấu trúc dữ liệu chia sẻ.

## Yêu cầu 5

  • Luồng quản lý dữ liệu thực hiện trí thông minh của cổng cảm biến Tóm lại, nó đọc các phép đo cảm biến từ dữ liệu chia sẻ, tính toán giá trị trung bình động của nhiệt độ và sử dụng kết quả đó để quyết định 'quá nóng/lạnh'. Nó không ghi các giá trị trung bình động vào dữ liệu chia sẻ - chỉ sử dụng chúng để ra quyết định nội bộ.

## Yêu cầu 6

  • Luồng quản lý lưu trữ đọc các phép đo cảm biến từ cấu trúc dữ liệu chia sẻ và chèn chúng vào cơ sở dữ liệu SQL. Nếu kết nối đến cơ sở dữ liệu SQL thất bại, trình quản lý lưu trữ sẽ đợi một chút trước khi thử lại. Các phép đo cảm biến sẽ ở lại trong dữ liệu chia sẻ cho đến khi kết nối đến cơ sở dữ liệu hoạt động trở lại. Nếu kết nối không thành công sau 3 lần thử, cổng sẽ đóng.

## Yêu cầu 7

  • Tiến trình ghi log nhận các sự kiện log từ tiến trình chính sử dụng một FIFO có tên "logFifo". Nếu FIFO này không tồn tại khi khởi động tiến trình chính hoặc tiến trình log, nó sẽ được tạo bởi một trong các tiến trình. Tất cả các luồng của tiến trình chính có thể tạo ra các sự kiện log và ghi các sự kiện log này vào FIFO. Điều này có nghĩa là FIFO được chia sẻ bởi nhiều luồng và do đó, việc truy cập vào FIFO phải an toàn cho luồng.

## Yêu cầu 8

  • Một sự kiện log chứa một thông điệp thông tin ASCII mô tả loại sự kiện. Đối với mỗi sự kiện log nhận được, tiến trình log ghi một thông điệp ASCII có định dạng <số thứ tự> <timestamp> <thông điệp thông tin sự kiện log> vào một dòng mới trên tệp log có tên "gateway.log".

## Yêu cầu 9

  • Ít nhất các sự kiện log sau cần được hỗ trợ:

1. Từ trình quản lý kết nối: - Một nút cảm biến với <sensorNodeID> đã mở một kết nối mới - Nút cảm biến với <sensorNodeID> đã đóng kết nối

2. Từ trình quản lý dữ liệu: - Nút cảm biến với <sensorNodeID> báo cáo quá lạnh (nhiệt độ trung bình = <giá trị>) - Nút cảm biến với <sensorNodeID> báo cáo quá nóng (nhiệt độ trung bình = <giá trị>) - Nhận dữ liệu cảm biến với ID nút cảm biến không hợp lệ <node-ID>

3. Từ trình quản lý lưu trữ: - Kết nối đến máy chủ SQL đã được thiết lập - Bảng mới <tên-bảng> đã được tạo - Kết nối đến máy chủ SQL bị mất - Không thể kết nối đến máy chủ SQL

PROCESS

  • Sau khi tôi chia sẻ về các yêu cầu tối thiếu để có thể làm 1 bài kia thì tôi sẽ phân tích theo ý hiểu của tôi về bài và cách giải quyết của tôi:

  • Hệ thống bao gồm hai thành phần chính:

    1. Server (main.c): Server đóng vai trò là gateway, quản lý việc kết nối với các sensor node và xử lý dữ liệu. Server có những chức năng chính sau:
  • Quản lý kết nối: Server sử dụng socket TCP để lắng nghe và chấp nhận kết nối từ các sensor node. Mỗi sensor node được định danh bằng một ID duy nhất.

  • Xử lý dữ liệu: Khi nhận được dữ liệu từ sensor (nhiệt độ và độ ẩm), server sẽ:

    • Lưu trữ vào bộ nhớ tạm thời

    • Ghi log vào file gateway.log

    • Lưu trữ vào database SQLite

  • Đa luồng: Server sử dụng nhiều thread để xử lý song song:

    • Thread connection_manager: Quản lý kết nối từ các sensor

    • Thread storage_manager: Định kỳ lưu dữ liệu vào database

    • Thread riêng cho mỗi sensor để xử lý message

  1. Sensor Node (sensor_node.c): Đây là chương trình giả lập cảm biến, có các chức năng:
  • Kết nối tới server qua TCP socket

  • Gửi ID để đăng ký với server

  • Định kỳ tạo dữ liệu giả (nhiệt độ 15-35°C, độ ẩm 30-100%) và gửi lên server

  • Format dữ liệu dạng: "SENSOR:id,TEMP:value,HUM:value"

Cấu trúc database khá đơn giản với bảng sensor_data gồm:

  • id: khóa chính tự tăng

  • sensor_id: ID của sensor

  • temperature: giá trị nhiệt độ

  • humidity: giá trị độ ẩm

  • timestamp: thời điểm ghi nhận

SERVER( Main.c)

  • Theo như tôi phân tích ở trên thì cần setup định nghĩa các biến toàn cục và cấu trúc để quản lý kết nối cảm biến và dữ liệu chia sẻ:
static volatile int keep_running = 1;

typedef struct {
    int id;
    int socket_fd;
    char ip[INET_ADDRSTRLEN];
    int port;
    double humidity;
} SensorConnection;

typedef struct {
    pthread_mutex_t mutex;
    int connected_sensors[MAX_SENSORS];
    double running_temps[MAX_SENSORS];
    double running_humidity[MAX_SENSORS];
    int sql_connected;
    int should_exit;
    SensorConnection sensor_connections[MAX_SENSORS];
    int connection_count;
    pthread_mutex_t conn_mutex;
    int port;
    sqlite3 *db;
    int sql_retry_count;
    pthread_mutex_t sql_mutex;
} SharedData;
  • Cấu trúc SensorConnection:

    • Đại diện mỗi kết nối của cảm biến.

    • Lưu trữ thông tin ID, IP, cổng, và độ ẩm.

  • Cấu trúc SharedData:

    • Chứa các biến chia sẻ giữa các thread như dữ liệu cảm biến, trạng thái database.

    • Mutex: Đảm bảo an toàn trong quá trình truy cập dữ liệu chia sẻ.

💡
Mutex (viết tắt của "mutual exclusion") là một cơ chế đồng bộ hóa được sử dụng trong lập trình đa luồng để ngăn chặn nhiều luồng truy cập đồng thời vào một tài nguyên chung, chẳng hạn như một biến hoặc một đoạn mã quan trọng. Mutex đảm bảo rằng chỉ một luồng có thể truy cập tài nguyên tại một thời điểm, giúp tránh các vấn đề liên quan đến truy cập đồng thời như race condition. Mutex process gồm pthread_mutex_init khởi tạo mutex lock. pthread_mutex_lock khóa mutex trước khi vào đoạn mã quan trọng. pthread_mutex_unlock mở khóa mutex sau khi hoàn thành đoạn mã quan trọng. pthread_mutex_destroy hủy mutex sau khi tất cả các luồng đã hoàn thành.
void handle_signal(int signum) {
    write_log("Received signal %d, cleaning up...", signum);
    keep_running = 0;
}

Xử lý các tín hiệu hệ thống một cách một cách an toàn.

Hàm connection_manager

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1)
{
    write_log("Failed to create socket");
    return NULL;
}

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(shared->port);

if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
{
    write_log("Failed to bind socket");
    close(server_fd);
    return NULL;
}

if (listen(server_fd, LISTEN_BACKLOG) == -1)
{
    write_log("Failed to listen on socket");
    close(server_fd);
    return NULL;
}
  • Khởi tạo socket server:

    1. Tạo socket: Dùng giao thức TCP/IP (SOCK_STREAM).

    2. Găn socket vào cổng: Sử dụng bind để lắng nghe kết nối.

    3. Chế độ lắng nghe: Giới hạn số kết nối backlog qua liste

int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1)
{
    write_log("Failed to accept connection");
    continue;
}

char buffer[BUFF_SIZE];
int bytes_read = read(client_fd, buffer, BUFF_SIZE);
if (bytes_read <= 0)
{
    close(client_fd);
    continue;
}

int sensor_id;
if (sscanf(buffer, "ID:%d", &sensor_id) != 1)
{
    write_log("Invalid sensor ID format");
    close(client_fd);
    continue;
}

pthread_mutex_lock(&shared->mutex);
SensorConnection* new_conn = &shared->sensor_connections[shared->connection_count];
new_conn->id = sensor_id;
new_conn->socket_fd = client_fd;
inet_ntop(AF_INET, &client_addr.sin_addr, new_conn->ip, INET_ADDRSTRLEN);
new_conn->port = ntohs(client_addr.sin_port);
write_log("Sensor node %d has opened a new connection from %s:%d", sensor_id, new_conn->ip, new_conn->port);

pthread_mutex_unlock(&shared->mutex);
  • Ở hàm này:

    1. Chấp nhận kết nối: Nhận socket client.

    2. Đọc ID: Sử dụng sscanf phân tích chuỗi gởi "ID:X".

    3. Lưu trữ: Thêm thông tin kết nối vào danh sách.

    4. Tạo thread: Gọi pthread_create để xử lý message từ cảm biến.

Hàm write_log

void write_log(const char* format, ...) {
    static pthread_mutex_t fifo_mutex = PTHREAD_MUTEX_INITIALIZER;
    char message[MAX_LOG_MSG];
    va_list args;

    memset(message, 0, sizeof(message));

    va_start(args, format);
    vsnprintf(message, sizeof(message) - 2, format, args);
    va_end(args);

    size_t len = strlen(message);
    if (len > 0 && message[len-1] != '\n') {
        strcat(message, "\n");
    }

    pthread_mutex_lock(&fifo_mutex);
    int fd = open(FIFO_NAME, O_WRONLY);
    if (fd != -1) {
        ssize_t bytes_written = write(fd, message, strlen(message));
        if (bytes_written < 0) {
            perror("write to FIFO failed");
        }
        close(fd);
    }
    pthread_mutex_unlock(&fifo_mutex);
}
  • Hàm write_log ghi các thông điệp nhật ký vào FIFO.

  • Tính năng:

    • Kéo dài thread an toàn khi ghi file log.

    • Ghi log qua FIFO để xử lý đồng bộ.

Hàm storage_manager

void* storage_manager(void* arg) {
    SharedData* shared = (SharedData*)arg;
    int retry_count = 0;
    const int MAX_RETRIES = 3;

    while (!shared->should_exit) {
        pthread_mutex_lock(&shared->mutex);

        if (!shared->sql_connected) {
            if (retry_count < MAX_RETRIES) {
                if (shared->db) {
                    sqlite3_close(shared->db);
                    shared->db = NULL;
                }

                int rc = sqlite3_open("sensor_data.db", &shared->db);
                if (rc == SQLITE_OK) {
                    shared->sql_connected = 1;
                    retry_count = 0;
                    write_log("Connection to SQL server established");

                    const char *create_table_sql =
                        "CREATE TABLE IF NOT EXISTS sensor_data ("
                        "id INTEGER PRIMARY KEY AUTOINCREMENT,"
                        "sensor_id INTEGER,"
                        "temperature REAL,"
                        "humidity REAL,"
                        "timestamp DATETIME DEFAULT CURRENT_TIMESTAMP);";

                    char *err_msg = NULL;
                    rc = sqlite3_exec(shared->db, create_table_sql, NULL, NULL, &err_msg);
                    if (rc != SQLITE_OK) {
                        write_log("SQL error: %s", err_msg);
                        sqlite3_free(err_msg);
                    } else {
                        write_log("New table sensor_data created");
                    }
                } else {
                    retry_count++;
                    write_log("Unable to connect to SQL server (attempt %d of %d)",
                              retry_count, MAX_RETRIES);
                }
            }
        }

        if (shared->sql_connected) {
            for (int i = 0; i < shared->connection_count; i++) {
                int sensor_id = shared->sensor_connections[i].id;
                if (shared->connected_sensors[sensor_id]) {
                    double temp = shared->running_temps[sensor_id];
                    double humidity = shared->running_humidity[sensor_id];
                    insert_sensor_data(shared, sensor_id, temp, humidity);
                }
            }
        }

        pthread_mutex_unlock(&shared->mutex);
        sleep(5);
    }

    if (shared->db) {
        sqlite3_close(shared->db);
        shared->db = NULL;
    }

    return NULL;
}
  • Hàm storage_manager xử lý việc lưu trữ dữ liệu cảm biến vào cơ sở dữ liệu SQLite.

Hàm main

    pid_t pid = fork();
    if (pid == 0) {
        log_process();
        exit(0);
    }
  • Tiến trình ghi log được khởi động (bằng fork) như một tiến trình con của tiến trình chính.
💡
Tiến trình fork được gọi là tiến trình cha mẹ (parent process). Tiến trình mới tạo ra dược gọi là tiến trình con (Child process)

![](cdn.hashnode.com/res/hashnode/image/upload/.. align="center")

SENSOR NODE (sensor_node.c)

#define BUFF_SIZE 1024
#define SERVER_IP "192.168.121.134"
  • BUFF_SIZE được định nghĩa là 1024, là kích thước của bộ đệm dùng để gửi tin nhắn.

  • SERVER_IP được định nghĩa là địa chỉ IP của máy chủ mà nút cảm biến sẽ kết nối tới.

typedef struct
{
    float temperature;
    float humidity;
} SensorData;
  • Cấu trúc SensorData: Cấu trúc này chứa các giá trị nhiệt độ và độ ẩm được tạo ra bởi cảm biến.

Hàm generate_sensor_data

SensorData generate_sensor_data(int sensor_id)
{
    srand(time(NULL) + sensor_id); 
    SensorData data;
    data.temperature = 15.0 + ((float)rand() / RAND_MAX) * 20.0; // 15-35°C
    data.humidity = 30.0 + ((float)rand() / RAND_MAX) * 70.0;    // 30-100%
    return data;
}
  • Hàm generate_sensor_data sẽ giả lập nhiệt độ, độ ẩm ngẫu nhiên:

    • srand(time(NULL) + sensor_id): Khởi tạo bộ sinh số ngẫu nhiên với thời gian hiện tại và ID cảm biến để đảm bảo các giá trị là duy nhất.

    • data.temperature: Tạo ra nhiệt độ ngẫu nhiên trong khoảng từ 15°C đến 35°C.

    • data.humidity: Tạo ra độ ẩm ngẫu nhiên trong khoảng từ 30% đến 100%.

Hàm main

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf("Usage: %s <sensor_id> <server_port>\n", argv[0]);
        printf("Example: %s 0 6000\n", argv[0]);
        exit(1);
    }

    int sensor_id = atoi(argv[1]);
    int server_port = atoi(argv[2]);
  • Chương trình yêu cầu hai tham số dòng lệnh: sensor_idserver_port.

    • Nếu không có đủ tham số, chương trình sẽ in ra thông báo hướng dẫn sử dụng và thoát.

    • sensor_idserver_port được chuyển đổi từ tham số dòng lệnh.

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        perror("Socket creation failed");
        exit(1);
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);

    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0)
    {
        perror("Invalid address");
        exit(1);
    }
  • Tạo socket: Tạo một socket TCP bằng cách sử dụng socket(AF_INET, SOCK_STREAM, 0).

    • Nếu việc tạo socket thất bại, chương trình sẽ in ra thông báo lỗi và thoát.
  • Cấu hình địa chỉ máy chủ: Cấu hình cấu trúc địa chỉ máy chủ.

    • server_addr.sin_family: Đặt loại địa chỉ là AF_INET.

    • server_addr.sin_port: Đặt số cổng bằng cách sử dụng htons(server_port).

    • inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr): Chuyển đổi địa chỉ IP của máy chủ từ dạng văn bản sang dạng nhị phân.

    char id_msg[10];
    sprintf(id_msg, "ID:%d", sensor_id);
    send(sock, id_msg, strlen(id_msg), 0);
  • Gửi ID cảm biến: Gửi ID cảm biến tới máy chủ.

    • sprintf(id_msg, "ID:%d", sensor_id): Định dạng ID cảm biến thành một chuỗi.

    • send(sock, id_msg, strlen(id_msg), 0): Gửi thông điệp chứa ID cảm biến tới máy chủ.

    while (1)
    {
        SensorData data = generate_sensor_data(sensor_id);

        char message[BUFF_SIZE];
        sprintf(message, "SENSOR:%d,TEMP:%.2f,HUM:%.2f",
                sensor_id, data.temperature, data.humidity);

        if (send(sock, message, strlen(message), 0) < 0)
        {
            printf("Sensor node %d: Connection lost\n", sensor_id);
            break;
        }

        printf("Sensor %d sent - Temperature: %.2f°C, Humidity: %.2f%%\n",
               sensor_id, data.temperature, data.humidity);

        sleep(5); // Gửi dữ liệu mỗi 5 giây
    }

    close(sock);
    return 0;
}
  • Vòng lặp chính: Liên tục tạo và gửi dữ liệu cảm biến tới máy chủ.

    • generate_sensor_data(sensor_id): Tạo dữ liệu cảm biến ngẫu nhiên.

    • sprintf(message, "SENSOR:%d,TEMP:%.2f,HUM:%.2f", sensor_id, data.temperature, data.humidity): Định dạng dữ liệu cảm biến thành một chuỗi.

    • send(sock, message, strlen(message), 0): Gửi thông điệp chứa dữ liệu cảm biến tới máy chủ.

    • Nếu kết nối bị mất, chương trình sẽ in ra thông báo và thoát khỏi vòng lặp.

    • printf("Sensor %d sent - Temperature: %.2f°C, Humidity: %.2f%%\n", sensor_id, data.temperature, data.humidity): In ra dữ liệu đã gửi.

    • sleep(5): Chờ 5 giây trước khi gửi dữ liệu tiếp theo.

    • Đóng socket: Đóng kết nối socket khi vòng lặp kết thúc.

CC = gcc
CFLAGS = -Wall -Wextra -pthread -I$(INC_DIR)
LDFLAGS = -pthread -lsqlite3

CUR_DIR := .
INC_DIR := $(CUR_DIR)/inc
SRC_DIR := $(CUR_DIR)/src
OBJ_DIR := $(CUR_DIR)/obj
BIN_DIR := $(CUR_DIR)/bin
# Object files
OBJ_FILES = $(OBJ_DIR)/log.o $(OBJ_DIR)/connection_manager.o $(OBJ_DIR)/sensor_handler.o $(OBJ_DIR)/storage_manager.o
# Targets
SERVER = server
SENSOR = sensor_node

make_dir:
    mkdir -p $(OBJ_DIR) $(BIN_DIR)


# Server
$(SERVER): $(CUR_DIR)/main.o $(OBJ_FILES)
    $(CC) $^ -o $@ $(LDFLAGS)
# Sensor Node
$(SENSOR): $(CUR_DIR)/sensor_node.o $(OBJ_FILES)
    $(CC) $^ -o $@ $(LDFLAGS)

# Object files
create_obj:
    $(CC) $(CFLAGS) -c -fPIC $(SRC_DIR)/log.c -o $(OBJ_DIR)/log.o
    $(CC) $(CFLAGS) -c -fPIC $(SRC_DIR)/connection_manager.c -o $(OBJ_DIR)/connection_manager.o
    $(CC) $(CFLAGS) -c -fPIC $(SRC_DIR)/sensor_handler.c -o $(OBJ_DIR)/sensor_handler.o
    $(CC) $(CFLAGS) -c -fPIC $(SRC_DIR)/storage_manager.c -o $(OBJ_DIR)/storage_manager.o
    $(CC) $(CFLAGS) -c -fPIC $(CUR_DIR)/main.c -o $(CUR_DIR)/main.o
    $(CC) $(CFLAGS) -c -fPIC $(CUR_DIR)/sensor_node.c -o $(CUR_DIR)/sensor_node.o 

all: create_obj make_dir $(SERVER) $(SENSOR)  
clean:
    rm -f *.o $(SERVER) $(SENSOR) gateway.log logFifo sensor_data.db
    rm -rf $(OBJ_DIR)/*.o
    rm -rf $(BIN_DIR)/*
.PHONY: all clean make_dir create_obj

HOW IT WORKS?

  • make để chạy chương trình

    • ./server port để chạy server

    • ./sensor_node port để chạy sensor node

    • file log: gateway.log

    • file fifo: logFifo

    • file database: sensor_data.db

      1. sqlite3 sensor_data.db

      2. SELECT * FROM sensor_data;

Tạo server

Tạo giả lập sensor node

Check ở gateway.log

Lưu trữ thông tin ở SQLite

Link Full code trên Github: https://github.com/Lordapk9/Sensor_Monitoring_System.git

VẬY LÀ XONG. CŨNG DỄ NHỈ :)))