首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >实验室多工位监测:UDP协议以太网温湿度记录仪,轻量服务器接收程序

实验室多工位监测:UDP协议以太网温湿度记录仪,轻量服务器接收程序

原创
作者头像
BJ盛世宏博小程
修改于 2026-09-28 11:09:46
修改于 2026-09-28 11:09:46
1090
举报

实验室多工位监测:UDP协议以太网温湿度记录仪,轻量服务器接收程序

一、实验室多工位监测的场景特征

实验室环境与机房、配电室、车间有一个本质区别:监测点位的分布是"工位级"的,而非"区域级"。

一台步入式试验室可能只需要2–3个监测点,但一个大型环境实验室往往同时运行几十台小型温箱、恒温槽、培养箱、老化架,每个设备旁边都需要独立监测。这些点位的特征是:

维度

机房/配电室

实验室多工位

点位数量

几十到几百,集中部署

几十到上百,分散在不同实验室

点位密度

每机柜1–2个

每台设备1个,设备间距大

网络条件

专用监控网络,布线规范

共用实验室办公网,端口有限

变更频率

低(季度级)

高(实验方案调整,设备频繁移动)

运维要求

7×24无人值守

有人值守,可接受轻量维护

数据需求

长期存档、告警联动

实验周期内连续记录、实验结束后导出

这些特征决定了技术方案的取舍方向:

  • UDP而非TCP:工位频繁变动,TCP的连接管理(IP绑定、端口占用、超时断开)反而成为负担。UDP无连接,设备换个位置、换个IP,服务器无需任何配置变更。
  • 轻量而非重型:不需要Kafka、不需要K8s、不需要复杂的服务编排。一个二进制文件跑起来就行,出了问题实验室工程师能自己重启。
  • 实验周期导向:数据按实验批次组织,而非按设备持续存储。实验开始→采集启动→实验结束→数据导出→采集停止。


二、方案总体设计

代码语言:javascript
复制
┌─────────────────────────────────────────────────────────────────────┐
│ 实验室A(恒温实验室)                                               │
│                                                                     │
│  [工位1] 温箱#1 ──┐                                                 │
│  [工位2] 温箱#2 ──┤                                                 │
│  [工位3] 恒温槽#1 ─┼── 实验室交换机 ──┐                            │
│  [工位4] 培养箱#3 ─┘                 │                            │
│                                      │                            │
├──────────────────────────────────────┼────────────────────────────┤
│ 实验室B(老化实验室)                 │                            │
│                                      │                            │
│  [工位5-20] 老化架×4 ──── 交换机 ───┼── 核心交换机               │
│                                      │                            │
├──────────────────────────────────────┼────────────────────────────┤
│ 实验室C(EMC实验室)                  │                            │
│                                      │                            │
│  [工位21-30] 屏蔽室内的设备 ── 光纤 ─┘                            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
                              │
                    ┌─────────▼─────────┐
                    │ 轻量接收服务器     │
                    │ (Linux, 无显示器) │
                    │                   │
                    │  udp_recv (主程序) │
                    │  ├── UDP监听      │
                    │  ├── 报文解析     │
                    │  ├── 实验会话管理 │
                    │  ├── SQLite存储   │
                    │  └── HTTP API     │
                    │                   │
                    │  资源占用:         │
                    │  CPU < 2%         │
                    │  RAM < 30MB       │
                    │  Disk ~按实验规模  │
                    └─────────┬─────────┘
                              │
              ┌───────────────┼───────────────┐
              │               │               │
        ┌─────▼─────┐  ┌─────▼─────┐  ┌─────▼─────┐
        │ 实验员PC  │  │ 实验员PC  │  │  数据导出  │
        │ 浏览器    │  │ 浏览器    │  │  CSV/Excel │
        │ 实时查看  │  │ 实时查看  │  │  实验报告  │
        └───────────┘  └───────────┘  └───────────┘

核心设计决策:

决策

选择

理由

传输协议

UDP广播/单播

无连接,设备移动无需重新配置

存储引擎

SQLite

零配置、单文件、实验室环境足够

服务框架

单二进制+systemd

部署简单,不需要容器

数据视图

实验会话(Session)

按实验组织数据,而非按设备持续记录

接口

内嵌HTTP API

浏览器直接访问,无需安装客户端

配置

命令行参数+JSON文件

实验室工程师可编辑


三、UDP报文协议

3.1 报文格式(紧凑二进制)

实验室场景对带宽不敏感,但对解析效率和设备端实现简单性有要求。采用固定28字节二进制格式:

代码语言:javascript
复制
┌──────────┬──────────┬────────────┬──────────────────────────────────┐
│ 偏移     │ 长度     │ 字段       │ 说明                             │
├──────────┼──────────┼────────────┼──────────────────────────────────┤
│ 0        │ 2        │ 魔数       │ 0x55AA(帧同步)                 │
│ 2        │ 1        │ 版本       │ 0x01                            │
│ 3        │ 1        │ 设备类型   │ 0x20=实验室记录仪               │
│ 4        │ 4        │ 设备ID     │ 唯一标识(MAC后4字节或自编)     │
│ 8        │ 4        │ 序列号     │ 设备内递增,用于去重             │
│ 12       │ 4        │ 温度       │ IEEE754 float, ℃                │
│ 16       │ 4        │ 湿度       │ IEEE754 float, %RH              │
│ 20       │ 2        │ 状态字     │ Bit0=正常, Bit1=低温告警...     │
│ 22       │ 4        │ 时间戳     │ Unix时间戳(设备RTC)            │
│ 26       │ 2        │ CRC16      │ 校验(含魔数到时间戳全部字节)    │
│ 总计     │ 28字节   │            │                                  │
└──────────┴──────────┴────────────┴──────────────────────────────────┘

3.2 设备端发送逻辑

代码语言:javascript
复制
// 设备固件中的UDP发送代码(伪代码)
void send_env_udp(void) {
    static uint32_t seq = 0;
    env_packet_t pkt;

    pkt.magic = 0x55AA;
    pkt.version = 0x01;
    pkt.dev_type = 0x20;
    pkt.dev_id = get_dev_id();  // 从Flash或MAC获取
    pkt.seq = seq++;
    pkt.temp = read_temperature();
    pkt.humi = read_humidity();
    pkt.status = get_status();
    pkt.timestamp = get_rtc_unix();
    pkt.crc = crc16((uint8_t*)&pkt, 26);

    // 发送到组播地址(实验室网络推荐组播而非广播)
    udp_send(&pkt, sizeof(pkt),
              MCAST_IP,  // 239.255.2.100
              MCAST_PORT // 9100
    );
}

为什么用组播而非广播:实验室网络通常与其他办公网络共用,广播会产生不必要的流量。组播让只有加入组播组的接收者才能收到数据。


四、轻量服务器接收程序

4.1 程序架构

代码语言:javascript
复制
┌────────────────────────────────────────────────────────────┐
│ udp_recv (单进程,事件驱动)                               │
│                                                            │
│  ┌─────────────┐    ┌──────────────┐    ┌──────────────┐ │
│  │ UDP接收线程  │───▶│ 解析/去重    │───▶│ 会话路由     │ │
│  │ (非阻塞IO)  │    │ (CRC+序列号) │    │ (dev→session)│ │
│  └─────────────┘    └──────────────┘    └──────┬───────┘ │
│                                                  │         │
│  ┌─────────────┐    ┌──────────────┐           │         │
│  │ HTTP API    │◀───│ 查询/导出    │◀──────────┘         │
│  │ (内置)      │    │ (SQLite)     │                     │
│  └─────────────┘    └──────────────┘                     │
│                                                            │
│  ┌─────────────┐                                          │
│  │ 会话管理    │  实验开始→创建session→绑定设备           │
│  │ (内存表)    │  实验结束→关闭session→导出数据           │
│  └─────────────┘                                          │
└────────────────────────────────────────────────────────────┘

4.2 核心数据结构

代码语言:javascript
复制
// session.h
#ifndef SESSION_H
#define SESSION_H

#include <stdint.h>
#include <time.h>

#define MAX_DEVICES_PER_SESSION  32
#define MAX_SESSIONS             16
#define DEV_ID_LEN               8

typedef struct {
    uint32_t dev_id;
    char     dev_label[32];      // 用户自定义标签,如"温箱#3-上层"
    uint32_t last_seq;           // 去重:上次收到的序列号
    time_t   last_recv_time;     // 最后收到数据的时间
    int      active;             // 是否活跃
} device_slot_t;

typedef struct {
    uint32_t session_id;
    char     session_name[64];   // 实验名称,如"高温老化-20250115"
    char     operator_name[32];  // 操作人
    time_t   start_time;
    time_t   end_time;
    int      active;             // 1=进行中, 0=已结束
    int      device_count;
    device_slot_t devices[MAX_DEVICES_PER_SESSION];
    char     db_path[256];       // 该session对应的SQLite文件路径
} session_t;

#endif

4.3 主程序(C语言实现)

代码语言:javascript
复制
// udp_recv.c
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "session.h"

#define LISTEN_PORT     9100
#define BUF_SIZE        1024
#define MAX_PACKETS     1000000

// 全局状态
static session_t sessions[MAX_SESSIONS];
static int session_count = 0;
static pthread_mutex_t sessions_mutex = PTHREAD_MUTEX_INITIALIZER;
static sqlite3 *current_db = NULL;  // 当前活跃session的数据库

// UDP报文结构
#pragma pack(push, 1)
typedef struct {
    uint16_t magic;
    uint8_t  version;
    uint8_t  dev_type;
    uint32_t dev_id;
    uint32_t seq;
    float    temp;
    float    humi;
    uint16_t status;
    uint32_t timestamp;
    uint16_t crc;
} env_packet_t;
#pragma pack(pop)

// CRC16-CCITT
static uint16_t crc16_ccitt(const uint8_t *data, size_t len) {
    uint16_t crc = 0xFFFF;
    for (size_t i = 0; i < len; i++) {
        crc ^= (uint16_t)data[i] << 8;
        for (int j = 0; j < 8; j++) {
            if (crc & 0x8000)
                crc = (crc << 1) ^ 0x1021;
            else
                crc <<= 1;
        }
    }
    return crc & 0xFFFF;
}

// 查找设备所在的session
static session_t *find_session_for_device(uint32_t dev_id) {
    for (int i = 0; i < session_count; i++) {
        if (!sessions[i].active) continue;
        for (int j = 0; j < sessions[i].device_count; j++) {
            if (sessions[i].devices[j].dev_id == dev_id)
                return &sessions[i];
        }
    }
    return NULL;
}

// 初始化SQLite数据库(新session)
static int init_session_db(session_t *sess) {
    char db_name[256];
    snprintf(db_name, sizeof(db_name),
             "/var/lib/udp_recv/%s_%lu.db",
             sess->session_name, (unsigned long)sess->start_time);
    strncpy(sess->db_path, db_name, sizeof(sess->db_path) - 1);

    if (current_db) {
        sqlite3_close(current_db);
    }

    if (sqlite3_open(db_name, &current_db) != SQLITE_OK) {
        fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(current_db));
        return -1;
    }

    const char *sql =
        "CREATE TABLE IF NOT EXISTS env_data ("
        "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
        "  dev_id INTEGER,"
        "  dev_label TEXT,"
        "  timestamp INTEGER,"
        "  temp REAL,"
        "  humi REAL,"
        "  status INTEGER,"
        "  seq INTEGER,"
        "  received_at INTEGER"
        ");"
        "CREATE INDEX IF NOT EXISTS idx_timestamp ON env_data(timestamp);"
        "CREATE INDEX IF NOT EXISTS idx_dev_id ON env_data(dev_id);";

    char *errmsg = NULL;
    if (sqlite3_exec(current_db, sql, NULL, NULL, &errmsg) != SQLITE_OK) {
        fprintf(stderr, "SQL error: %s\n", errmsg);
        sqlite3_free(errmsg);
        return -1;
    }

    printf("Session DB created: %s\n", db_name);
    return 0;
}

// 写入数据到SQLite
static int write_to_db(session_t *sess, device_slot_t *dev,
                       env_packet_t *pkt, time_t received_at) {
    if (!current_db) return -1;

    sqlite3_stmt *stmt;
    const char *sql =
        "INSERT INTO env_data (dev_id, dev_label, timestamp, temp, humi, status, seq, received_at) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";

    if (sqlite3_prepare_v2(current_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
        return -1;
    }

    sqlite3_bind_int(stmt, 1, pkt->dev_id);
    sqlite3_bind_text(stmt, 2, dev->dev_label, -1, SQLITE_STATIC);
    sqlite3_bind_int(stmt, 3, pkt->timestamp);
    sqlite3_bind_double(stmt, 4, pkt->temp);
    sqlite3_bind_double(stmt, 5, pkt->humi);
    sqlite3_bind_int(stmt, 6, pkt->status);
    sqlite3_bind_int(stmt, 7, pkt->seq);
    sqlite3_bind_int(stmt, 8, (int)received_at);

    int rc = sqlite3_step(stmt);
    sqlite3_finalize(stmt);

    return (rc == SQLITE_DONE) ? 0 : -1;
}

// 创建新session(通过命令行或HTTP API调用)
int create_session(const char *name, const char *operator,
                   uint32_t *dev_ids, int dev_count, char **labels) {
    pthread_mutex_lock(&sessions_mutex);

    if (session_count >= MAX_SESSIONS) {
        pthread_mutex_unlock(&sessions_mutex);
        return -1;
    }

    session_t *sess = &sessions[session_count];
    memset(sess, 0, sizeof(session_t));
    sess->session_id = session_count + 1;
    strncpy(sess->session_name, name, sizeof(sess->session_name) - 1);
    strncpy(sess->operator_name, operator, sizeof(sess->operator_name) - 1);
    sess->start_time = time(NULL);
    sess->active = 1;
    sess->device_count = dev_count;

    for (int i = 0; i < dev_count && i < MAX_DEVICES_PER_SESSION; i++) {
        sess->devices[i].dev_id = dev_ids[i];
        sess->devices[i].active = 1;
        if (labels && labels[i])
            strncpy(sess->devices[i].dev_label, labels[i],
                    sizeof(sess->devices[i].dev_label) - 1);
    }

    session_count++;
    pthread_mutex_unlock(&sessions_mutex);

    // 初始化数据库
    init_session_db(sess);

    printf("Session created: %s (ID=%u, devices=%d)\n",
           name, sess->session_id, dev_count);
    return sess->session_id;
}

// 关闭session
int close_session(int session_id) {
    pthread_mutex_lock(&sessions_mutex);

    for (int i = 0; i < session_count; i++) {
        if (sessions[i].session_id == session_id && sessions[i].active) {
            sessions[i].active = 0;
            sessions[i].end_time = time(NULL);
            printf("Session closed: %s (duration: %lds)\n",
                   sessions[i].session_name,
                   sessions[i].end_time - sessions[i].start_time);

            // 关闭数据库
            if (current_db) {
                sqlite3_close(current_db);
                current_db = NULL;
            }
            pthread_mutex_unlock(&sessions_mutex);
            return 0;
        }
    }

    pthread_mutex_unlock(&sessions_mutex);
    return -1;
}

// UDP接收主循环
static void *udp_receive_loop(void *arg) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t len = sizeof(cliaddr);
    char buffer[BUF_SIZE];

    // 创建UDP socket
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        pthread_exit(NULL);
    }

    // 设置SO_REUSEADDR
    int reuse = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    // 增大接收缓冲区
    int rcvbuf = 16 * 1024 * 1024;  // 16MB
    setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(LISTEN_PORT);

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        pthread_exit(NULL);
    }

    printf("UDP receiver listening on port %d\n", LISTEN_PORT);

    while (1) {
        int n = recvfrom(sockfd, buffer, BUF_SIZE, 0,
                         (struct sockaddr *)&cliaddr, &len);
        if (n < sizeof(env_packet_t)) {
            continue;  // 报文太短,丢弃
        }

        env_packet_t *pkt = (env_packet_t *)buffer;

        // 验证魔数
        if (pkt->magic != 0x55AA) {
            continue;
        }

        // CRC校验
        uint16_t calc_crc = crc16_ccitt((uint8_t *)pkt,
                                         sizeof(env_packet_t) - 2);
        if (calc_crc != pkt->crc) {
            continue;  // CRC错误,丢弃
        }

        // 查找该设备所属的session
        pthread_mutex_lock(&sessions_mutex);
        session_t *sess = find_session_for_device(pkt->dev_id);
        if (!sess) {
            pthread_mutex_unlock(&sessions_mutex);
            continue;  // 设备未绑定到任何session,丢弃
        }

        // 查找设备槽位
        device_slot_t *dev = NULL;
        for (int i = 0; i < sess->device_count; i++) {
            if (sess->devices[i].dev_id == pkt->dev_id) {
                dev = &sess->devices[i];
                break;
            }
        }

        if (!dev) {
            pthread_mutex_unlock(&sessions_mutex);
            continue;
        }

        // 去重(序列号检查)
        if (dev->last_seq == pkt->seq) {
            pthread_mutex_unlock(&sessions_mutex);
            continue;  // 重复包,丢弃
        }

        dev->last_seq = pkt->seq;
        dev->last_recv_time = time(NULL);
        pthread_mutex_unlock(&sessions_mutex);

        // 写入数据库
        time_t now = time(NULL);
        write_to_db(sess, dev, pkt, now);

        // 控制台输出(可选,生产环境可关闭)
        printf("[%u] dev=%08X temp=%.1f humi=%.1f seq=%u\n",
               (unsigned)sess->session_id, pkt->dev_id,
               pkt->temp, pkt->humi, pkt->seq);
    }

    close(sockfd);
    return NULL;
}

// HTTP API线程(轻量实现,使用libmicrohttpd)
#ifdef ENABLE_HTTP
#include <microhttpd.h>

static enum MHD_Result http_handler(void *cls, struct MHD_Connection *connection,
                                     const char *url, const char *method,
                                     const char *version, const char *upload_data,
                                     size_t *upload_data_size, void **con_cls) {
    // 处理API请求:
    // GET  /api/sessions          - 列出所有session
    // POST /api/sessions          - 创建新session
    // POST /api/sessions/{id}/close - 关闭session
    // GET  /api/sessions/{id}/data - 查询数据
    // GET  /api/sessions/{id}/export - 导出CSV

    // 具体实现略...
    return MHD_YES;
}
#endif

int main(int argc, char *argv[]) {
    printf("UDP Environment Data Receiver v1.0\n");
    printf("=====================================\n");

    // 创建数据目录
    system("mkdir -p /var/lib/udp_recv");

    // 启动UDP接收线程
    pthread_t udp_thread;
    pthread_create(&udp_thread, NULL, udp_receive_loop, NULL);

#ifdef ENABLE_HTTP
    // 启动HTTP服务器
    struct MHD_Daemon *httpd = MHD_start_daemon(
        MHD_USE_THREAD_PER_CONNECTION, 8080, NULL, NULL,
        &http_handler, NULL, MHD_OPTION_END);
    printf("HTTP API listening on port 8080\n");
#endif

    // 如果命令行指定了session,自动创建
    if (argc > 3) {
        // ./udp_recv "实验名称" "操作人" dev_id1,dev_id2,...
        // 解析参数并创建session
    }

    printf("Server running. Press Ctrl+C to stop.\n");

    // 等待信号
    while (1) {
        sleep(1);
    }

    return 0;
}

4.4 编译与部署

代码语言:javascript
复制
# 编译(需要SQLite3和可选的libmicrohttpd)
gcc -O2 -pthread -o udp_recv udp_recv.c -lsqlite3 -lmicrohttpd -DENABLE_HTTP

# 安装
cp udp_recv /usr/local/bin/
mkdir -p /var/lib/udp_recv
chmod 755 /usr/local/bin/udp_recv

# systemd服务文件
cat > /etc/systemd/system/udp-recv.service << 'EOF'
[Unit]
Description=UDP Environment Data Receiver
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/udp_recv
Restart=always
RestartSec=5
User=nobody
Group=nogroup
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable udp-recv
systemctl start udp-recv

五、HTTP API设计

5.1 API接口

方法

路径

说明

GET

/api/sessions

列出所有session

POST

/api/sessions

创建新session

POST

/api/sessions/{id}/close

关闭session

GET

/api/sessions/{id}/data

查询数据(支持时间范围、设备过滤)

GET

/api/sessions/{id}/export

导出CSV

GET

/api/devices

列出所有活跃设备

GET

/api/status

服务器状态

5.2 创建Session示例

代码语言:javascript
复制
curl -X POST http://localhost:8080/api/sessions \
  -H "Content-Type: application/json" \
  -d '{
    "name": "高温老化-20250115",
    "operator": "张三",
    "devices": [
      {"id": 0x00000041, "label": "温箱#1-上层"},
      {"id": 0x00000042, "label": "温箱#1-下层"},
      {"id": 0x00000043, "label": "恒温槽#2"}
    ]
  }'

5.3 查询数据示例

代码语言:javascript
复制
curl "http://localhost:8080/api/sessions/1/data?start=1700000000&end=1700086400&dev_id=0x00000041"

返回:

代码语言:javascript
复制
{
  "session": "高温老化-20250115",
  "device": "温箱#1-上层",
  "count": 2880,
  "data": [
    {"ts": 1700000000, "temp": 85.2, "humi": 45.0, "status": 1},
    {"ts": 1700000005, "temp": 85.3, "humi": 45.1, "status": 1}
  ]
}

5.4 导出CSV

代码语言:javascript
复制
curl "http://localhost:8080/api/sessions/1/export" > experiment_data.csv

CSV格式:

代码语言:javascript
复制
timestamp,device,temperature,humidity,status
2025-01-15T08:00:00,温箱#1-上层,85.2,45.0,1
2025-01-15T08:00:05,温箱#1-上层,85.3,45.1,1
2025-01-15T08:00:10,温箱#1-上层,85.2,44.9,1

六、前端页面(轻量)

内嵌一个简单的HTML页面,无需额外安装:

代码语言:javascript
复制
<!-- 内嵌在HTTP服务器中,访问 http://server:8080/ 即可 -->
<!DOCTYPE html>
<html>
<head>
<title>实验室环境监测</title>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.session { border: 1px solid #ddd; padding: 15px; margin: 10px 0; }
.active { border-color: #4CAF50; }
.data-table { width: 100%; border-collapse: collapse; }
.data-table th, .data-table td { border: 1px solid #ddd; padding: 8px; }
.data-table th { background: #f2f2f2; }
.chart { width: 100%; height: 300px; }
</style>
</head>
<body>
<h1>实验室环境监测</h1>
<div id="sessions"></div>
<div id="chart"></div>

<script>
// 从API加载数据并绘制简单图表
fetch('/api/sessions')
  .then(r => r.json())
  .then(data => {
    const div = document.getElementById('sessions');
    data.sessions.forEach(s => {
      const cls = s.active ? 'session active' : 'session';
      div.innerHTML += `<div class="${cls}">
        <h3>${s.name}</h3>
        <p>操作人: ${s.operator}</p>
        <p>设备数: ${s.device_count}</p>
        <p>状态: ${s.active ? '进行中' : '已结束'}</p>
        <button onclick="exportCSV(${s.id})">导出CSV</button>
      </div>`;
    });
  });

function exportCSV(id) {
  window.open(`/api/sessions/${id}/export`);
}
</script>
</body>
</html>

七、性能与资源占用

7.1 资源消耗

代码语言:javascript
复制
场景:30台设备,每5秒上报一次

CPU:
  UDP接收: ~0.5% (单核)
  数据库写入: ~0.3% (批量提交)
  HTTP服务: ~0.2% (空闲时)
  总计: < 2%

内存:
  程序代码: ~2MB
  Session数据: ~4KB × 16 sessions = 64KB
  SQLite缓存: ~2MB (默认page_cache)
  总计: < 30MB

磁盘:
  单条记录: ~40字节 (SQLite行)
  30设备 × 每5秒1条 × 3600秒 = 21600条/小时 = ~840KB/小时
  24小时实验: ~20MB

7.2 系统限制

限制项

默认值

说明

最大session数

16

可调整

每session最大设备数

32

可调整

SQLite并发写入

单线程

实验室场景足够

UDP接收缓冲区

16MB

可调整


八、排障指南

8.1 常见问题

问题

排查方法

收不到数据

检查UDP端口监听:netstat -anup \| grep 9100

数据未写入数据库

检查session是否创建、设备是否绑定

数据库锁定

SQLite并发写入冲突,检查是否有多个进程同时写

丢包

检查接收缓冲区:cat /proc/net/udp

时间不对

检查设备RTC和服务器NTP同步

8.2 调试命令

代码语言:javascript
复制
# 查看UDP接收统计
cat /proc/net/udp

# 抓包验证
tcpdump -i eth0 udp port 9100 -vv

# 查看SQLite数据库内容
sqlite3 /var/lib/udp_recv/experiment.db
.tables
SELECT * FROM env_data LIMIT 10;

# 查看服务状态
systemctl status udp-recv
journalctl -u udp-recv -f

九、经验总结

  1. 实验室场景需要"实验会话"概念。数据不是持续产生的,而是围绕实验周期组织的。按session管理数据,实验结束后可以独立导出、归档、删除,不会和其他实验的数据混在一起。
  2. UDP无连接特性在工位频繁变动时优势明显。设备从一个实验室搬到另一个实验室,IP变了,服务器不需要任何配置修改,只要设备发送的数据能被路由到服务器,就能自动被接收。
  3. SQLite是实验室场景的理想存储引擎。零配置、单文件、ACID事务、支持SQL查询,实验结束后整个数据库文件可以直接拷贝带走。
  4. 轻量不代表简陋。内嵌HTTP API让浏览器成为操作界面,实验室工程师不需要安装任何客户端软件。
  5. 去重是必须的。UDP不可靠传输,网络抖动可能导致重复包。序列号去重确保每条数据只记录一次。

关键词:实验室,多工位监测,UDP协议,温湿度记录仪,轻量服务器,接收程序,实验会话,SQLite,HTTP API,数据导出

标签:#实验室 #多工位 #UDP #温湿度记录仪 #轻量服务器 #数据采集 #SQLite #实验会话 #HTTP API #环境监测

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 实验室多工位监测:UDP协议以太网温湿度记录仪,轻量服务器接收程序
    • 一、实验室多工位监测的场景特征
    • 二、方案总体设计
    • 三、UDP报文协议
      • 3.1 报文格式(紧凑二进制)
      • 3.2 设备端发送逻辑
    • 四、轻量服务器接收程序
      • 4.1 程序架构
      • 4.2 核心数据结构
      • 4.3 主程序(C语言实现)
      • 4.4 编译与部署
    • 五、HTTP API设计
      • 5.1 API接口
      • 5.2 创建Session示例
      • 5.3 查询数据示例
      • 5.4 导出CSV
    • 六、前端页面(轻量)
    • 七、性能与资源占用
      • 7.1 资源消耗
      • 7.2 系统限制
    • 八、排障指南
      • 8.1 常见问题
      • 8.2 调试命令
    • 九、经验总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档