设备接入指南
从第一条连接到第一条数据:按步骤将设备接入工地监控平台。
使用 TCP 上报,让设备出现在监控后台
网页的 HTTP 地址仅用于打开后台;设备应连接服务器 IP 与 TCP 端口,不发送 HTTP 请求。
01 / 连接与注册
本机使用 127.0.0.1:9000;其他电脑使用服务器局域网 IP,例如 192.168.1.10:9000。0.0.0.0 是监听配置,不是客户端连接地址。确保系统防火墙允许此 TCP 端口。
建立长连接后先发送注册报文,收到 ok: true 后再上报。新编号会自动创建设备;建议使用 EXAMPLE-001,避开模拟器占用的 TC-001 至 TC-006。
同编号新连接会替换旧连接;每条连接只允许绑定一个设备。断开连接或 30 秒内无有效报文时判定离线。默认每 10 秒发送心跳,每 3 秒上报数据;有效上报同样刷新在线时间。
02 / 可复制报文
下列报文均为单行 JSON。复制后在末尾追加实际换行 LF(0x0A),而不是文本字符 \n。支持 CRLF;JSON 不应跨行美化后发送。
注册
{"type":"register","deviceId":"EXAMPLE-001"}
心跳
{"type":"heartbeat","deviceId":"EXAMPLE-001"}
遥测上报
{"type":"telemetry","deviceId":"EXAMPLE-001","messageId":"sample-001","collectedAt":"2026-10-14T17:10:24.5760255+00:00","motorTemperature":55.2,"ambientTemperature":26.5,"load":3.8,"ratedLoad":8,"hookHeight":38.5,"radius":24,"rotation":120,"windSpeed":4.2,"workState":"running"}每条新采集记录使用新的 messageId;网络重试可复用原编号,服务端按设备与报文编号去重。示例采集时间为页面生成时刻,实际设备请使用带 Z 或时区偏移的 ISO 8601 时间。
成功应答(上报)
{"type":"ack","ok":true,"code":"ok","messageId":"sample-001","message":"数据已保存"}失败应答(未注册)
{"type":"ack","ok":false,"code":"not_registered","messageId":"sample-001","message":"请先注册,设备编号必须与连接一致"}注册和心跳成功返回 code: ok、message: 接收成功;重复数据返回 ok: true, code: duplicate,不重复入库。其他错误码:invalid_json、invalid_utf8、validation、frame_too_large。超限报文应答后关闭连接,其余错误可修正后重发;非法报文不会刷新心跳。
03 / 字段与单位
| 字段 | 适用报文 | 类型 / 单位 | 要求 |
|---|---|---|---|
| type | 全部 | string | 必填:register / heartbeat / telemetry |
| deviceId | 全部 | string | 必填:1–64 位字母、数字、下划线、连字符 |
| messageId | telemetry | string | 必填:1–128 字符,设备内唯一 |
| collectedAt | telemetry | ISO 8601 | 必填:带 Z 或时区偏移;入库存储 UTC,界面北京时间 |
| motorTemperature | telemetry | number / °C | 必填:有限数字;电机温度,超过 80°C 告警 |
| ambientTemperature | telemetry | number / °C | 必填:有限数字;环境温度 |
| load | telemetry | number / t | 必填:有限数字;实际载荷 ≥ 0,超过 ratedLoad 告警 |
| ratedLoad | telemetry | number / t | 必填:有限数字;额定载荷 > 0 |
| hookHeight | telemetry | number / m | 必填:有限数字;吊钩高度 ≥ 0 |
| radius | telemetry | number / m | 必填:有限数字;工作幅度 ≥ 0 |
| rotation | telemetry | number / ° | 必填:有限数字;回转角度 0–360 |
| windSpeed | telemetry | number / m/s | 必填:有限数字;风速 ≥ 0,超过 12m/s 告警 |
| workState | telemetry | string | 必填:idle(待机)、running(运行)、fault(故障并告警) |
04 / 可运行的 C# 示例
安装 .NET 10 SDK,下载源码 ZIP 并解压,在 CraneClient 目录运行以下命令。参数依次为主机、端口、设备编号、上报间隔、心跳间隔;Ctrl+C 停止。连接中断后客户端自动重试。
启动命令
dotnet run -- 127.0.0.1 9000 EXAMPLE-001 3 10
Program.cs(与下载文件一致)
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
// dotnet run -- 127.0.0.1 9000 EXAMPLE-001 [reportSeconds] [heartbeatSeconds]
var host = args.ElementAtOrDefault(0) ?? "127.0.0.1";
var port = int.Parse(args.ElementAtOrDefault(1) ?? "9000");
var deviceId = args.ElementAtOrDefault(2) ?? "EXAMPLE-001";
var reportSeconds = double.Parse(args.ElementAtOrDefault(3) ?? "3", System.Globalization.CultureInfo.InvariantCulture);
var heartbeatSeconds = double.Parse(args.ElementAtOrDefault(4) ?? "10", System.Globalization.CultureInfo.InvariantCulture);
if (reportSeconds <= 0 || heartbeatSeconds <= 0 || port is < 1 or > 65535) throw new ArgumentException("Invalid interval or port");
using var quit = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; quit.Cancel(); };
var token = quit.Token;
while (!token.IsCancellationRequested)
{
try
{
using var client = new TcpClient();
await client.ConnectAsync(host, port, token);
Console.WriteLine($"Connected to TCP {host}:{port}, device {deviceId}");
using var writer = new StreamWriter(client.GetStream(), new UTF8Encoding(false), leaveOpen: true) { AutoFlush = true, NewLine = "\n" };
using var reader = new StreamReader(client.GetStream(), Encoding.UTF8, leaveOpen: true);
// A single send path avoids heartbeat and telemetry interleaving bytes.
async Task Send(object packet)
{
await writer.WriteLineAsync(JsonSerializer.Serialize(packet).AsMemory(), token);
using var ackTimeout = CancellationTokenSource.CreateLinkedTokenSource(token);
ackTimeout.CancelAfter(TimeSpan.FromSeconds(10));
var line = await reader.ReadLineAsync(ackTimeout.Token) ?? throw new IOException("Server disconnected");
Console.WriteLine("ACK " + line);
using var ack = JsonDocument.Parse(line);
if (!ack.RootElement.GetProperty("ok").GetBoolean()) throw new IOException("Server rejected packet: " + line);
}
await Send(new { type = "register", deviceId });
var nextReport = DateTime.UtcNow;
var nextHeartbeat = DateTime.UtcNow.AddSeconds(heartbeatSeconds);
while (!token.IsCancellationRequested)
{
var now = DateTime.UtcNow;
if (now >= nextHeartbeat)
{
await Send(new { type = "heartbeat", deviceId });
nextHeartbeat = DateTime.UtcNow.AddSeconds(heartbeatSeconds);
}
if (now >= nextReport)
{
await Send(new { type = "telemetry", deviceId, messageId = Guid.NewGuid().ToString("N"), collectedAt = DateTimeOffset.UtcNow, motorTemperature = 55.2, ambientTemperature = 26.5, load = 3.8, ratedLoad = 8, hookHeight = 38.5, radius = 24, rotation = 120, windSpeed = 4.2, workState = "running" });
nextReport = DateTime.UtcNow.AddSeconds(reportSeconds);
}
await Task.Delay(100, token);
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested) { break; }
catch (Exception ex)
{
Console.WriteLine(ex.Message + "; reconnecting in 2 seconds");
try { await Task.Delay(2000, token); } catch (OperationCanceledException) { break; }
}
}
05 / TCP 调试与结果验证
常见问题
端口不可达或连接被拒绝
确认服务端已启动、端口为 9000、IP 正确,且防火墙允许 TCP。远程客户端不能使用 127.0.0.1 或 0.0.0.0。端口被其他进程占用会使服务端启动失败,查看日志。
已发送报文,却没有应答
检查末尾是否含实际 LF 字节。TCP 不保留消息边界,未发送换行时服务端会等待完整报文,超时后关闭连接。
JSON 格式错误或指标校验失败
使用双引号和有限数字,不要带注释或尾逗号。telemetry 所有字段均必填,按上方字段表检查。使用 UTF-8 无 BOM 编码。
提示未注册即上报
每次连接或重连都需要注册。心跳和上报的 deviceId 必须与该连接注册的编号一致。
设备编号重复或连接突然断开
同编号最新连接会替换旧连接。模拟器使用 TC-001 至 TC-006,外部示例请使用独立设备编号。
设备心跳超时
默认 30 秒内需要至少一条有效注册、心跳或遥测报文。建议心跳间隔 10 秒;错误报文不会延长在线时间。