首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >元器件车间仓储区:RJ45温湿度记录仪,TCP/IP本地存储与远端同步

元器件车间仓储区:RJ45温湿度记录仪,TCP/IP本地存储与远端同步

原创
作者头像
盛世宏博科技
发布于 2026-09-28 10:52:50
发布于 2026-09-28 10:52:50
1270
举报

元器件车间仓储区:RJ45温湿度记录仪,TCP/IP本地存储与远端同步

近期在调试一套工业环境监控系统时,遇到了关于Modbus连接保持的问题。现场部署了一批网口温湿度变送器,PoE取电、Modbus TCP上云,同时利旧接入存量RS485探头,并开启了SNMP和UDP Trap服务。元器件车间仓储区对温湿度有硬性合规要求——电子元器件吸潮后焊接良率直接受影响,仓储环境必须可追溯、可审计、不可篡改。但车间网络并非永远可靠:交换机割接、VLAN调整、光缆熔接,都可能导致断网。这篇讲一种工程上已经验证的架构——设备侧本地循环存储 + TCP/IP断点续传 + 远端时序库同步,确保"网络断了数据不丢,网络恢复自动补齐"。


一、架构总览

代码语言:javascript
复制
┌─────────────────────────────────────────────────────────────┐
│                    设备侧(记录仪)                           │
│                                                             │
│  ┌──────────┐    ┌──────────┐    ┌──────────────────────┐  │
│  │ 传感器    │───→│ 采集任务  │───→│ 本地存储管理器        │  │
│  │ 采样1Hz   │    │ 5s/次    │    │ Flash循环缓冲区       │  │
│  └──────────┘    └──────────┘    │ 12字节/条 × 4096条    │  │
│                                   └──────────┬───────────┘  │
│                                              │              │
│                              ┌───────────────▼───────────┐  │
│                              │ TCP/IP 同步客户端           │  │
│                              │ 连接管理 + 补传协议         │  │
│                              └───────────────┬───────────┘  │
└──────────────────────────────────────────────┼──────────────┘
                                               │ TCP 502/9000
                    ┌──────────────────────────▼────────────┐
                    │          车间本地服务器                 │
                    │  ┌─────────────┐   ┌───────────────┐  │
                    │  │ 接收服务     │   │ 时序数据库     │  │
                    │  │ TCP 502      │──→│ InfluxDB      │  │
                    │  └─────────────┘   └───────┬───────┘  │
                    │                             │          │
                    │  ┌─────────────┐            │          │
                    │  │ 去重引擎     │←───────────┘          │
                    │  │ dev+ts主键   │                      │
                    │  └─────────────┘                      │
                    └────────────────────────────────────────┘
                                     │
                              ┌──────▼──────┐
                              │  云端备份    │
                              │  (异步同步)  │
                              └─────────────┘

二、设备侧本地存储设计

2.1 Flash循环缓冲区

元器件仓储要求数据至少保留30天。以5秒采样间隔计算:

代码语言:javascript
复制
30天 × 24小时 × 3600秒 ÷ 5秒 = 518,400条
518,400条 × 12字节/条 = 6,220,800字节 ≈ 6MB

选用8MB SPI Flash(如W25Q64),划分两个逻辑区:

区域

大小

用途

系统区

64KB

配置、校准参数、设备ID

数据区

7.9MB

循环存储温湿度记录

代码语言:javascript
复制
// storage.h
#ifndef _STORAGE_H_
#define _STORAGE_H_

#include <stdint.h>
#include <stdbool.h>

#define FLASH_PAGE_SIZE      256
#define FLASH_SECTOR_SIZE     4096
#define RECORD_SIZE           12     // 每条记录12字节
#define RECORDS_PER_SECTOR    (FLASH_SECTOR_SIZE / RECORD_SIZE)  // 341条/扇区
#define DATA_SECTOR_COUNT     1984   // 7.9MB / 4KB
#define DATA_START_SECTOR     16      // 跳过系统区

// 单条记录结构(12字节,packed)
typedef struct __attribute__((packed)) {
    uint32_t collect_ts;      // 采集时间戳(Unix epoch)
    int16_t  temperature;     // 温度 ×100
    uint16_t humidity;        // 湿度 ×10
    uint16_t voltage;         // 供电电压 ×100
    uint8_t  status;         // 状态位
    uint8_t  reserved;        // 保留,对齐用
} env_record_t;               // 12字节

// 循环缓冲区元数据(存储在RAM,掉电不丢因为有Flash镜像)
typedef struct {
    uint32_t write_sector;    // 当前写入扇区号
    uint32_t write_offset;    // 扇区内偏移(记录数)
    uint32_t total_records;   // 总记录数(循环计数)
    uint32_t oldest_ts;       // 最旧记录的时间戳
    uint32_t newest_ts;       // 最新记录的时间戳
    uint16_t dev_addr;        // 设备地址
} ring_buffer_meta_t;

int storage_init(void);
int storage_write_record(const env_record_t *record);
int storage_read_record(uint32_t index, env_record_t *record);
int storage_read_range(uint32_t start_ts, uint32_t end_ts, 
                       env_record_t *out_buf, uint32_t max_count,
                       uint32_t *actual_count);
int storage_get_unsynced(uint32_t last_sync_ts, 
                         env_record_t *out_buf, uint32_t max_count,
                         uint32_t *actual_count);
void storage_format(void);
uint32_t storage_get_total_stored(void);

#endif

2.2 写入策略:攒批 + 磨损均衡

Flash写入有寿命限制(W25Q64典型10万次擦写)。如果每条记录都擦写,8MB Flash在5秒间隔下约3个月就会磨损完。策略:

  1. RAM缓存:采集任务写入RAM环形缓冲区(32条),满32条或超时1秒后批量写入Flash
  2. 扇区级写入:Flash按页(256字节)编程,一次写32条 = 384字节,分两页写
  3. 磨损均衡:循环使用所有数据扇区,不固定写同一区域
代码语言:javascript
复制
// storage.c 核心实现
#include "storage.h"
#include "w25qxx.h"
#include "crc16.h"

static ring_buffer_meta_t g_meta;
static env_record_t g_ram_cache[32];
static uint8_t g_cache_count = 0;
static uint32_t g_last_flush_ms = 0;

#define CACHE_FLUSH_INTERVAL_MS  1000   // 1秒强制刷盘
#define CACHE_THRESHOLD          32     // 满32条刷盘

int storage_init(void) {
    w25qxx_init();
    
    // 读取元数据(从Flash最后一个扇区恢复)
    uint8_t meta_buf[sizeof(ring_buffer_meta_t)];
    w25qxx_read_data((DATA_START_SECTOR + DATA_SECTOR_COUNT) * FLASH_SECTOR_SIZE,
                     meta_buf, sizeof(meta_buf));
    
    memcpy(&g_meta, meta_buf, sizeof(g_meta));
    
    // 校验元数据有效性
    uint16_t crc = crc16_modbus(meta_buf, sizeof(g_meta) - 2);
    if (crc != *(uint16_t *)(meta_buf + sizeof(g_meta) - 2)) {
        // 元数据损坏,重新格式化
        storage_format();
    }
    
    return 0;
}

int storage_write_record(const env_record_t *record) {
    // 写入RAM缓存
    memcpy(&g_ram_cache[g_cache_count], record, RECORD_SIZE);
    g_cache_count++;
    
    uint32_t now = sys_now();
    
    // 条件满足时刷入Flash
    if (g_cache_count >= CACHE_THRESHOLD || 
        (now - g_last_flush_ms) >= CACHE_FLUSH_INTERVAL_MS) {
        return flush_to_flash();
    }
    
    return 0;
}

static int flush_to_flash(void) {
    if (g_cache_count == 0) return 0;
    
    // 计算写入地址
    uint32_t addr = (g_meta.write_sector * FLASH_SECTOR_SIZE) + 
                    (g_meta.write_offset * RECORD_SIZE);
    
    // 如果当前扇区写满,擦除下一个扇区
    if (g_meta.write_offset >= RECORDS_PER_SECTOR) {
        uint32_t next_sector = (g_meta.write_sector - DATA_START_SECTOR + 1) % DATA_SECTOR_COUNT 
                               + DATA_START_SECTOR;
        
        // 擦除(Flash擦除是阻塞的,约30-50ms)
        w25qxx_erase_sector(next_sector * FLASH_SECTOR_SIZE);
        
        g_meta.write_sector = next_sector;
        g_meta.write_offset = 0;
        addr = next_sector * FLASH_SECTOR_SIZE;
    }
    
    // 批量写入
    uint8_t write_buf[RECORD_SIZE * CACHE_THRESHOLD];
    for (int i = 0; i < g_cache_count; i++) {
        memcpy(write_buf + i * RECORD_SIZE, &g_ram_cache[i], RECORD_SIZE);
    }
    
    w25qxx_write_data(addr, write_buf, g_cache_count * RECORD_SIZE);
    
    // 更新元数据
    g_meta.write_offset += g_cache_count;
    g_meta.total_records++;
    g_meta.newest_ts = g_ram_cache[g_cache_count - 1].collect_ts;
    if (g_meta.total_records <= DATA_SECTOR_COUNT * RECORDS_PER_SECTOR) {
        g_meta.oldest_ts = g_ram_cache[0].collect_ts;
    }
    
    // 持久化元数据到Flash最后一个扇区
    persist_meta();
    
    g_cache_count = 0;
    g_last_flush_ms = sys_now();
    
    return 0;
}

static void persist_meta(void) {
    uint8_t buf[sizeof(ring_buffer_meta_t)];
    memcpy(buf, &g_meta, sizeof(g_meta));
    uint16_t crc = crc16_modbus(buf, sizeof(g_meta) - 2);
    *(uint16_t *)(buf + sizeof(g_meta) - 2) = crc;
    
    w25qxx_erase_sector((DATA_START_SECTOR + DATA_SECTOR_COUNT) * FLASH_SECTOR_SIZE);
    w25qxx_write_data((DATA_START_SECTOR + DATA_SECTOR_COUNT) * FLASH_SECTOR_SIZE,
                      buf, sizeof(buf));
}

三、TCP/IP同步协议设计

3.1 补传协议帧格式

代码语言:javascript
复制
┌──────────────────────────────────────────────────────────┐
│  REPLAY_REQ (客户端→服务端)                              │
│  ┌──────┬────────┬──────────┬───────────────────────┐   │
│  │header│dev_addr│start_ts  │ end_ts (0=最新)        │   │
│  │0xAA01│1B      │4B        │ 4B                    │   │
│  └──────┴────────┴──────────┴───────────────────────┘   │
│  总计: 11字节                                             │
├──────────────────────────────────────────────────────────┤
│  REPLAY_GRANT (服务端→客户端)                            │
│  ┌──────┬────────┬──────────┬───────────────────────┐   │
│  │header│dev_addr│total_cnt │ max_seq               │   │
│  │0xAA02│1B      │4B        │ 4B (分片用)           │   │
│  └──────┴────────┴──────────┴───────────────────────┘   │
│  总计: 11字节                                             │
├──────────────────────────────────────────────────────────┤
│  REPLAY_DATA (客户端→服务端, 可多帧)                     │
│  ┌──────┬────────┬──────────┬────────┬──────────────┐   │
│  │header│dev_addr│seq_no    │count   │ records...    │   │
│  │0xAA03│1B      │4B        │1B      │ count×12B    │   │
│  └──────┴────────┴──────────┴────────┴──────────────┘   │
│  总计: 8 + count×12 字节                                  │
├──────────────────────────────────────────────────────────┤
│  REPLAY_ACK (服务端→客户端)                              │
│  ┌──────┬────────┬──────────┬───────────────────────┐   │
│  │header│dev_addr│seq_no    │ status (0=OK 1=ERR)   │   │
│  │0xAA04│1B      │4B        │ 1B                    │   │
│  └──────┴────────┴──────────┴───────────────────────┘   │
│  总计: 8字节                                              │
└──────────────────────────────────────────────────────────┘

3.2 设备端同步状态机

代码语言:javascript
复制
// sync_client.c
typedef enum {
    SYNC_IDLE = 0,
    SYNC_CONNECTING,
    SYNC_WAIT_GRANT,
    SYNC_SENDING,
    SYNC_WAIT_ACK,
    SYNC_COMPLETE,
    SYNC_FAILED,
} sync_state_t;

static sync_state_t g_sync_state = SYNC_IDLE;
static uint32_t g_last_sync_ts = 0;      // 上次同步到的最后时间戳
static uint32_t g_last_connect_attempt = 0;
static uint8_t g_retry_count = 0;

#define SYNC_RETRY_INTERVAL_MS  30000     // 30秒重试
#define SYNC_TIMEOUT_MS         10000     // 10秒超时
#define SYNC_MAX_RETRIES        5

void sync_task(void) {
    uint32_t now = sys_now();
    
    switch (g_sync_state) {
        case SYNC_IDLE:
            // 定期检查网络是否可达
            if (network_is_up() && (now - g_last_connect_attempt) > SYNC_RETRY_INTERVAL_MS) {
                g_sync_state = SYNC_CONNECTING;
                g_last_connect_attempt = now;
                tcp_connect_sync_server();
            }
            break;
            
        case SYNC_CONNECTING:
            // TCP连接中,等待回调
            if (now - g_last_connect_attempt > SYNC_TIMEOUT_MS) {
                g_sync_state = SYNC_FAILED;
            }
            break;
            
        case SYNC_WAIT_GRANT:
            // 已发送REPLAY_REQ,等待GRANT
            if (now - g_grant_sent_time > SYNC_TIMEOUT_MS) {
                g_retry_count++;
                if (g_retry_count >= SYNC_MAX_RETRIES) {
                    g_sync_state = SYNC_FAILED;
                } else {
                    resend_replay_req();
                }
            }
            break;
            
        case SYNC_SENDING:
            // 发送DATA帧
            send_next_data_frame();
            g_sync_state = SYNC_WAIT_ACK;
            break;
            
        case SYNC_WAIT_ACK:
            if (now - g_data_sent_time > SYNC_TIMEOUT_MS) {
                // 重传当前帧
                resend_current_frame();
            }
            break;
            
        case SYNC_COMPLETE:
            g_retry_count = 0;
            g_last_sync_ts = g_current_sync_ts;
            save_last_sync_ts(g_last_sync_ts);  // 持久化到Flash
            tcp_close_sync_connection();
            g_sync_state = SYNC_IDLE;
            break;
            
        case SYNC_FAILED:
            g_retry_count = 0;
            tcp_close_sync_connection();
            g_sync_state = SYNC_IDLE;
            break;
    }
}

// TCP连接成功回调
void on_sync_connected(void) {
    g_sync_state = SYNC_WAIT_GRANT;
    send_replay_req();
    g_grant_sent_time = sys_now();
}

// 收到REPLAY_GRANT回调
void on_replay_grant(uint32_t total_count, uint32_t max_seq) {
    g_total_to_send = total_count;
    g_total_seqs = max_seq;
    g_current_seq = 0;
    g_sync_state = SYNC_SENDING;
}

// 收到REPLAY_ACK回调
void on_replay_ack(uint32_t seq_no, uint8_t status) {
    if (status == 0 && seq_no == g_current_seq) {
        g_current_seq++;
        if (g_current_seq >= g_total_seqs) {
            g_sync_state = SYNC_COMPLETE;
        } else {
            g_sync_state = SYNC_SENDING;
        }
    } else {
        // 重传
        g_sync_state = SYNC_SENDING;
    }
}

3.3 发送DATA帧

代码语言:javascript
复制
#define RECORDS_PER_FRAME  32    // 每帧最多32条 = 384字节 + 8字节头 = 392字节

static void send_next_data_frame(void) {
    if (!g_sync_pcb || !g_sync_pcb->state == ESTABLISHED) return;
    
    uint8_t tx_buf[8 + RECORDS_PER_FRAME * RECORD_SIZE];
    
    // 帧头
    tx_buf[0] = 0xAA; tx_buf[1] = 0x03;                    // header
    tx_buf[2] = g_dev_addr;                                  // dev_addr
    tx_buf[3] = (g_current_seq >> 24) & 0xFF;               // seq MSB
    tx_buf[4] = (g_current_seq >> 16) & 0xFF;
    tx_buf[5] = (g_current_seq >> 8) & 0xFF;
    tx_buf[6] = g_current_seq & 0xFF;                        // seq LSB
    
    // 从Flash读取数据
    uint32_t records_in_frame = RECORDS_PER_FRAME;
    env_record_t records[RECORDS_PER_FRAME];
    uint32_t actual = read_records_for_sync(g_current_seq * RECORDS_PER_FRAME,
                                            records, records_in_frame);
    
    if (actual == 0) {
        g_sync_state = SYNC_COMPLETE;
        return;
    }
    
    tx_buf[7] = (uint8_t)actual;                             // count
    
    // 填充记录
    for (int i = 0; i < actual; i++) {
        memcpy(tx_buf + 8 + i * RECORD_SIZE, &records[i], RECORD_SIZE);
    }
    
    // TCP发送
    struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, 8 + actual * RECORD_SIZE, PBUF_RAM);
    if (p) {
        memcpy(p->payload, tx_buf, 8 + actual * RECORD_SIZE);
        tcp_write(g_sync_pcb, p->payload, p->len, TCP_WRITE_FLAG_COPY);
        tcp_output(g_sync_pcb);
        pbuf_free(p);
    }
    
    g_data_sent_time = sys_now();
}

static uint32_t read_records_for_sync(uint32_t index, env_record_t *out, uint32_t max_count) {
    // 从Flash按时间顺序读取未同步的记录
    // 使用g_last_sync_ts作为起点
    return storage_read_range(g_last_sync_ts + 1, 0xFFFFFFFF,
                              out, max_count, &actual);
}

四、服务端接收与去重

4.1 TCP接收服务

代码语言:javascript
复制
#!/usr/bin/env python3
"""
TCP同步接收服务 - 元器件仓储温湿度记录仪
"""
import asyncio
import struct
import socket
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Dict, List, Optional
import asyncpg
from influxdb_client import InfluxDBClient, Point, WritePrecision


@dataclass
class SyncSession:
    dev_addr: int
    total_count: int = 0
    max_seq: int = 0
    received_seqs: set = field(default_factory=set)
    records: List[dict] = field(default_factory=list)
    state: str = "WAIT_REQ"


class TCPSyncServer:
    """TCP同步服务器"""
    
    HEADER_REPLAY_REQ = 0xAA01
    HEADER_REPLAY_GRANT = 0xAA02
    HEADER_REPLAY_DATA = 0xAA03
    HEADER_REPLAY_ACK = 0xAA04
    
    def __init__(self, host: str, port: int, influx_url: str, influx_token: str, 
                 influx_org: str, influx_bucket: str, pg_dsn: str):
        self.host = host
        self.port = port
        self.sessions: Dict[tuple, SyncSession] = {}
        
        # InfluxDB
        self.influx_client = InfluxDBClient(url=influx_url, token=influx_token, org=influx_org)
        self.influx_write = self.influx_client.write_api()
        self.influx_bucket = influx_bucket
        self.influx_org = influx_org
        
        # PostgreSQL(用于去重索引)
        self.pg_dsn = pg_dsn
        self.pg_pool = None
    
    async def start(self):
        self.pg_pool = await asyncpg.create_pool(self.pg_dsn)
        # 初始化去重表
        async with self.pg_pool.acquire() as conn:
            await conn.execute("""
                CREATE TABLE IF NOT EXISTS env_record_dedup (
                    dev_addr    SMALLINT,
                    collect_ts  BIGINT,
                    sync_time   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    PRIMARY KEY (dev_addr, collect_ts)
                );
                CREATE INDEX IF NOT EXISTS idx_dedup_dev_ts 
                ON env_record_dedup(dev_addr, collect_ts);
            """)
        
        server = await asyncio.start_server(self.handle_client, self.host, self.port)
        print(f"TCP同步服务器监听 {self.host}:{self.port}")
        async with server:
            await server.serve_forever()
    
    async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
        addr = writer.get_extra_info('peername')
        print(f"客户端连接: {addr}")
        
        session = SyncSession(dev_addr=0)
        self.sessions[addr] = session
        
        try:
            while True:
                data = await reader.read(4096)
                if not data:
                    break
                
                await self.process_frame(data, session, writer)
                
        except asyncio.IncompleteReadError:
            pass
        except Exception as e:
            print(f"客户端{addr} 异常: {e}")
        finally:
            writer.close()
            await writer.wait_closed()
            self.sessions.pop(addr, None)
            print(f"客户端断开: {addr}")
    
    async def process_frame(self, data: bytes, session: SyncSession, writer: asyncio.StreamWriter):
        if len(data) < 3:
            return
        
        header = struct.unpack(">H", data[:2])[0]
        dev_addr = data[2]
        session.dev_addr = dev_addr
        
        if header == self.HEADER_REPLAY_REQ:
            # 解析请求
            start_ts, end_ts = struct.unpack(">II", data[3:11])
            
            # 查询待同步记录数
            async with self.pg_pool.acquire() as conn:
                # 获取未同步的记录数(简化:从InfluxDB查最后同步时间)
                # 实际工程用PostgreSQL记录同步状态
                total_count = await conn.fetchval("""
                    SELECT COUNT(*) FROM env_records 
                    WHERE dev_addr = $1 AND collect_ts > $2
                """, dev_addr, start_ts)
                
                max_seq = (total_count + 31) // 32  # 向上取整
            
            # 发送GRANT
            grant_frame = struct.pack(">HBII", self.HEADER_REPLAY_GRANT, 
                                       dev_addr, total_count, max_seq)
            writer.write(grant_frame)
            await writer.drain()
            
            session.total_count = total_count
            session.max_seq = max_seq
            session.state = "WAIT_DATA"
            
        elif header == self.HEADER_REPLAY_DATA:
            seq_no = struct.unpack(">I", data[3:7])[0]
            count = data[7]
            
            if len(data) < 8 + count * 12:
                return
            
            # 解析记录
            records = []
            for i in range(count):
                offset = 8 + i * 12
                ts, temp_raw, hum_raw, volt_raw, status = \
                    struct.unpack(">IhHHBB", data[offset:offset + 12])
                
                records.append({
                    'dev_addr': dev_addr,
                    'collect_ts': ts,
                    'temperature': temp_raw / 100.0,
                    'humidity': hum_raw / 10.0,
                    'voltage': volt_raw / 100.0,
                    'status': status,
                })
            
            # 去重写入
            await self.write_with_dedup(records)
            
            # 发送ACK
            ack_frame = struct.pack(">HBIB", self.HEADER_REPLAY_ACK,
                                     dev_addr, seq_no, 0)
            writer.write(ack_frame)
            await writer.drain()
            
            session.received_seqs.add(seq_no)
            
            if len(session.received_seqs) >= session.max_seq:
                session.state = "COMPLETE"
    
    async def write_with_dedup(self, records: List[dict]):
        """去重写入:先写PostgreSQL去重索引,再写InfluxDB"""
        if not records:
            return
        
        async with self.pg_pool.acquire() as conn:
            async with conn.transaction():
                for r in records:
                    # 尝试插入去重表,冲突则跳过
                    inserted = await conn.execute("""
                        INSERT INTO env_record_dedup (dev_addr, collect_ts)
                        VALUES ($1, $2)
                        ON CONFLICT (dev_addr, collect_ts) DO NOTHING
                    """, r['dev_addr'], r['collect_ts'])
                    
                    if inserted == 'INSERT 0 1':
                        # 新记录,写入InfluxDB
                        point = Point("env_sensor") \
                            .tag("device", f"sensor_{r['dev_addr']:03d}") \
                            .tag("source", "sync_replay") \
                            .field("temperature", r['temperature']) \
                            .field("humidity", r['humidity']) \
                            .field("voltage", r['voltage']) \
                            .field("status", r['status']) \
                            .time(datetime.fromtimestamp(r['collect_ts'], tz=timezone.utc),
                                  WritePrecision.S)
                        
                        self.influx_write.write(bucket=self.influx_bucket,
                                                org=self.influx_org,
                                                record=point)


async def main():
    server = TCPSyncServer(
        host="0.0.0.0",
        port=502,
        influx_url="http://localhost:8086",
        influx_token="your-token",
        influx_org="your-org",
        influx_bucket="env_monitor",
        pg_dsn="postgresql://user:pass@localhost/env_monitor",
    )
    await server.start()


if __name__ == "__main__":
    asyncio.run(main())

五、NTP时钟校正

设备端RTC在断电后可能漂移,导致collect_ts不准确。同步时服务端需要校正:

代码语言:javascript
复制
// NTP客户端(LWIP SNTP)
#include "lwip/apps/sntp.h"

static uint32_t g_ntp_offset = 0;     // RTC与NTP的偏移量
static uint32_t g_last_ntp_sync = 0;

#define NTP_SYNC_INTERVAL_MS  3600000  // 1小时同步一次

void ntp_task(void) {
    uint32_t now = sys_now();
    
    if (now - g_last_ntp_sync > NTP_SYNC_INTERVAL_MS) {
        sntp_setservername(0, "pool.ntp.org");
        sntp_init();
        
        // 等待NTP响应
        for (int i = 0; i < 10; i++) {
            uint32_t ntp_time = sntp_get_current_timestamp();
            if (ntp_time > 0) {
                uint32_t rtc_time = get_rtc_timestamp();
                g_ntp_offset = ntp_time - rtc_time;
                g_last_ntp_sync = now;
                
                // 校正RTC
                set_rtc_offset(g_ntp_offset);
                break;
            }
            sys_msleep(1000);
        }
        
        sntp_stop();
    }
}

// 获取校正后的时间戳
uint32_t get_corrected_timestamp(void) {
    return get_rtc_timestamp() + g_ntp_offset;
}

六、断网检测与自动恢复

代码语言:javascript
复制
// link_monitor.c
static uint8_t g_link_up = 0;
static uint32_t g_last_server_contact = 0;
static uint8_t g_offline_mode = 0;

#define OFFLINE_THRESHOLD_MS  60000   // 60秒无服务端响应进入离线模式

void link_monitor_task(void) {
    uint32_t now = sys_now();
    
    // 检测物理链路
    if (ethernet_link_up()) {
        g_link_up = 1;
    } else {
        g_link_up = 0;
        g_offline_mode = 1;
        return;
    }
    
    // 检测TCP连接状态
    if (g_sync_state == SYNC_COMPLETE || g_sync_state == SYNC_IDLE) {
        g_last_server_contact = now;
        g_offline_mode = 0;
    } else if (g_sync_state == SYNC_FAILED) {
        if (now - g_last_server_contact > OFFLINE_THRESHOLD_MS) {
            g_offline_mode = 1;
        }
    }
    
    // 离线模式下,采集任务仍然写入Flash
    if (g_offline_mode) {
        // 提高采集频率?不,保持原频率,让Flash攒更多数据
        // 但可以降低UDP Trap发送频率,省电
        set_trap_interval(30000);  // 30秒
    } else {
        set_trap_interval(5000);   // 5秒
    }
}

七、排障清单

现象

原因

排查

数据断档

Flash写满后未正确循环

检查wear_leveling逻辑,确认扇区擦除顺序

同步超时

车间交换机STP阻塞

检查端口状态,配置PortFast

去重失效

PostgreSQL唯一约束未生效

检查表结构,确认dev_addr+collect_ts联合主键

时间戳跳变

RTC晶振漂移

检查NTP同步间隔,必要时换温补晶振

Flash写入卡顿

擦除扇区阻塞采集

用双缓冲或后台擦除,避免采集任务等待

TCP连接被拒

服务端端口被防火墙拦截

telnet server_ip 502测试

补传数据重复

ACK丢失导致客户端重传

服务端去重表正常工作


八、工程交付检查表

  • [ ] Flash分区表文档(系统区/数据区/元数据区)
  • [ ] 磨损均衡算法验证(模拟写入100万次)
  • [ ] 掉电保护测试(写入过程中断电,重启后数据不损坏)
  • [ ] 同步协议文档(帧格式、状态机、超时重试)
  • [ ] PostgreSQL去重表初始化脚本
  • [ ] InfluxDB retention policy配置(30天自动降采样)
  • [ ] NTP服务器配置(车间内网NTP或公网)
  • [ ] 离线模式验证(断网24小时,恢复后自动补传)
  • [ ] 监控面板(本地存储使用率、同步延迟、丢包率)
  • [ ] 告警规则(Flash剩余空间<10%、连续同步失败>5次)

九、关键词

元器件车间、仓储区、RJ45温湿度记录仪、TCP/IP本地存储、Flash循环缓冲区、断点续传、磨损均衡、远端同步、NTP时钟校正、去重引擎、离线模式、数据可追溯


十、一句话总结

元器件仓储区的温湿度记录仪,核心诉求是"数据不丢、可追溯、可审计":设备侧用Flash循环缓冲区本地存储(攒批写入+磨损均衡),网络恢复后通过TCP/IP补传协议(REPLAY_REQ→GRANT→DATA→ACK)按需回传,服务端用dev_addr+collect_ts联合主键去重,NTP校正时间戳确保时序一致,离线模式下持续采集不中断,恢复后自动补齐——这套架构在车间网络不稳定的环境下保证了数据的完整性和连续性。

物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控 #元器件仓储 #本地存储 #断点续传 #Flash #TCP同步 #工业物联网

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

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

目录
  • 元器件车间仓储区:RJ45温湿度记录仪,TCP/IP本地存储与远端同步
    • 一、架构总览
    • 二、设备侧本地存储设计
      • 2.1 Flash循环缓冲区
      • 2.2 写入策略:攒批 + 磨损均衡
    • 三、TCP/IP同步协议设计
      • 3.1 补传协议帧格式
      • 3.2 设备端同步状态机
      • 3.3 发送DATA帧
    • 四、服务端接收与去重
      • 4.1 TCP接收服务
    • 五、NTP时钟校正
    • 六、断网检测与自动恢复
    • 七、排障清单
    • 八、工程交付检查表
    • 九、关键词
    • 十、一句话总结
  • 物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控 #元器件仓储 #本地存储 #断点续传 #Flash #TCP同步 #工业物联网
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档