开源代码uSNMP推荐
码云源码路径:https://gitee.com/fensnote/uSNMP
注:源码作者不是我,我只是推荐~
uSNMP - 一个小型且可移植的 SNMP v1 ‘C’ 库
本文是对 uSNMP 库的介绍,并展示如何使用它使低成本硬件能够利用 SNMP 的强大功能用于物联网和资产监控项目。
uSNMP(“微型 SNMP”) 是一个小型且可移植的 ‘C’ 库,用于开发 SNMP v1 代理和管理器。源代码中包含了对 Arduino IDE、Windows 和 *nix 的移植,并且已经在兼容 Arduino 的(AVR ATmega328p)和带以太网屏蔽的 Arduino Mega、NodeMCU v0.9(Expressif ESP8266)、ESP32、Windows(使用 Embarcadero BCC32C C++ 编译器编译)和 Cygwin(使用 gcc)上进行了测试。
uSNMP 能有多小?
在带有以太网屏蔽的 Arduino ATmega328p 上,一个实现了 mib-2::system
表、三个极简表(2 个数字输入,状态切换时发送陷阱)、2 个数字输出和 1 个模拟输入的 uSNMP 代理大约为 20kB,包括 SPI、以太网、UDP、DNS 例程。它支持 Get、GetNext、Set
操作,并在任何数字输入切换时发送 Trap
。2kB 的 SRAM 限制了 MIB 条目的数量和网络数据包大小(从而限制了请求和响应长度)。通过放弃 mib-2::system
表,可以在相应的表中添加更多的数字和模拟 I/O 引脚。在 Arduino Mega、ESP8266 或 ESP32 上,由于 SRAM 大得多,可以支持更大的数据包缓冲区和更多的 I/O 引脚。
uSNMP 能做什么?
该库包括以下功能:以词典顺序存储和遍历 MIB 树;支持回调函数来获取和设置 MIB 叶节点的值,发出 Get、GetNext、Set
请求;构建和处理响应;创建和解析变量绑定列表,发送 Trap
并处理字节序问题。
为什么使用 SNMP?
SNMP(简单网络管理协议)是 IT 设备中的事实上的标准,在工业和建筑环境领域得到了很好的支持:网络设备、服务器和存储、UPS、整流器、远程保护或保护信号设备、RTU、远程 I/O 等。其在 ASN.1 符号的文本文件中定义的管理信息库(MIB)概念是其强大之处。
MIB 文件的作用类似于数据字典或设备描述语言。将新设备纳入基于 SNMP 的管理软件非常容易,这类软件有很多,包括开源软件,具有地理和拓扑地图叠加、仪表板、图表、事件日志、事件动作过滤器、故障单等功能。
这种设置非常适合物联网应用或资产管理,其中有许多相同的站点和设备,但点数很少。相比之下,SCADA/HMI 软件适合单点多点数的场景,如加工厂或建筑物,并具有更多的可视化功能,如 3D 和动画。
为什么是 SNMP v1,而不是 v2 或 v3?
尽管 SNMP 协议名字中有 “简单” 二字,但即使对于 SNMP v1,要实现它并将其适配到小型处理器上也并非易事。与 v2/v3 相比,使用 SNMP v1 会失去什么?主要是批量数据查询操作和安全功能。但请考虑:设备的 MIB 仍然可以通过 SNMP v1 操作完全遍历。而且,大多数(如果不是全部)工业协议,包括像 Modbus、BACnet 和 Profinet 这样的主流协议,都没有内置的安全功能或安全功能较弱。这并不是轻视安全性,而是在情况允许时倡导实用主义。
uSNMP 如何工作?
uSNMP 库扩展了 M. Tim Jones 所著《嵌入式系统的 TCP/IP 应用层协议》(Charles River Media,2002 年,ISBN 1-58450-247-9)一书第 8 章中介绍的嵌入式 SNMP 代理,他精辟地写道:
“……SNMP 消息生成的问题在于…… 前向(未知)TLV 长度…… 解决这个问题的方法是使用预测解析器解析 SNMP 请求,并在解析过程中构建响应…… 我们通过 SNMP PDU 进行预测性解析,当到达最终 TLV 时,我们通过函数调用链返回并更新所需 TLV 的长度值。”
如何使用 uSNMP?
有代理和命令行工具的代码示例,可用作开发 SNMP v1 代理、发出 SNMP v1 请求和处理响应以及发送陷阱的模板。
适用于 Windows 和 *nix 的示例 uSNMP 代理 usnmpd.c 从文件中读取 OID 和值对,通过让轮询程序格式化并将其接收的数据写入该文件,可用作 SNMP v1 网关。
另一个代理示例 usnmpd.ino 将 Arduino 板变成具有数字和模拟 I/O 的 SNMP 使能控制器。
MIB 文件在 mibs 目录中。ARDUINO.MIB 文件用于 Arduino 软件(IDE)管理的板,其私有企业编号(PEN)是 Armadino 的 38644(http://www.armadino.com)
Arduino ATmega328p 和 NodeMCU v0.9 上的测试电路:DI 在引脚 D2 上,DO 在 D6 上,AI 在 A0 上。
usnmpd.ino - Arduino 的 SNMP 代理
让我们更深入地了解 usnmpd.ino。对于第三方硬件包,如 NodeMCU 和 ESP32,首先需要在 Arduino IDE 中添加其板管理器 JSON 文件的 URL。这些 URL 指向 Arduino IDE 用于构建可用已安装板列表的 JSON 索引文件。这可以从 文件 > 首选项 中完成,对于像 NodeMCU 这样的 ESP8266 板:
将 ESP8266 包导入 Arduino IDE。
然后转到 工具 > 板 > 板管理器 并安装该包
将 ESP8266 包安装到 Arduino IDE。
setup()
usnmpd.ino 的前几行设置网络连接(以太网或 WiFi)、IP 地址和代理配置。
// 代理的 IP 配置。保留这些全局变量名。IPAddress hostIpAddr( 192, 168, 1, 177 ),  dnsServer( 192, 168, 1, 1 ),  hostGateway( 192, 168, 1, 1 ),  hostNetmask( 255, 255, 255, 0 );\#ifdef ARDUINO\_ETHERNETunsigned char hostMacAddr\[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };\#else // 假设是 ARDUINO\_WIFIchar staSSID\[] = "Wifi\_SSID";char staPSK\[] = "Wifi\_Password";\#endif// SNMP 代理配置。\#define ENTERPRISE\_OID "P.38644.30" // 用作 sysObjectID 和在陷阱中\#define RO\_COMMUNITY "public"  \#define RW\_COMMUNITY "private"\#define TRAP\_DST\_ADDR "192.168.1.170"
uSNMP 定义了三个对象 ID 前缀,每个有效的 OID 都必须以此为开头:
B 表示 Mgmt-Mib2 - 1.3.6.1.2.1E 表示 Experimental - 1.3.6.1.3P 表示 Private-Enterprises - 1.3.6.1.4.1
因此,sysDescr.0 (``1.3.6.1``.``2.1.1.1``.0)
将被编码为 “B.1.1.0”,而企业 OID 1.3.6.1``.4.1.38644.30
被编码为 “P.38644.30”。
setup()
初始化板和代理 “引擎”,包括构建 MIB 树和发送冷启动陷阱。根据目标微控制器上可用的 SRAM 数量,引脚 D2 到 D5 被指定为数字输入,D6 到 D8 为数字输出,A0 和 A1 为模拟输入。在包含 mib-2::system
表的情况下,带有 ATmega328p 的 Arduino UNO 可以有 D2、D3、D6、D7 和 A0,而如果需要,Arduino Mega 可能会超过 D5、D8 和 A1。否则,省略系统表将为 UNO 释放更多空间以容纳更多引脚。
initSnmpAgent(SNMP\_PORT, ENTERPRISE\_OID, RO\_COMMUNITY, RW\_COMMUNITY);initMibTree();trapBuild(\&request, enterpriseOID, hostIpAddr, COLD\_START, 0, NULL); // 冷启动陷阱trapSend(\&request, trapDstAddr, TRAP\_DST\_PORT, roCommunity);
MIB 树是通过 miblistadd()
函数构建的,该函数以词典顺序将 MIB 叶节点嫁接到树上。如果需要,随后设置节点的值,并附加回调函数以响应 SNMP Get 和 Set 操作。在下面的提取物中,sysDescr.0
被分配了一个已经包含系统描述的字符串。sysObjectID.0
在使用 BER(基本编码规则)编码后,用 EnterpriseOID 初始化。sysUpTime.0
设置了 get_uptime()
回调,以便在有 Get 操作请求时填写系统运行时间。
/\* 系统 MIB \*/// sysDescr 条目thismib = miblistadd(mibTree, "B.1.1.0", OCTET\_STRING, RD\_ONLY,  sysDescr, strlen(sysDescr));// sysObjectID 条目thismib = miblistadd(mibTree, "B.1.2.0", OBJECT\_IDENTIFIER, RD\_ONLY,  entOIDBer, 0); // 先将长度设为 0i = str2ber(enterpriseOID, entOIDBer);mibsetvalue(thismib, (void \*) entOIDBer, (int) i); // 设置适当的长度// sysUptime 条目thismib = miblistadd(mibTree, "B.1.3.0", TIMETICKS, RD\_ONLY, NULL, 0);i = 0; mibsetvalue(thismib, \&i, 0);mibsetcallback(thismib, get\_uptime, NULL);
数字和模拟 I/O 引脚在 SNMP 表中呈现。为了节省内存,这些表是极简的,仅包含索引和引脚值。需要回调函数,以便在响应 Get 或 Set 请求时及时检索值。因此,例如对于数字输出 D6:
// 数字输出 #6 索引thismib = miblistadd(mibTree, "P.38644.30.2.1.1.6", INTEGER, RD\_ONLY, NULL, 0);i = 6; mibsetvalue(thismib, \&i, 0);// 数字 #6 的值thismib = miblistadd(mibTree, "P.38644.30.2.1.2.6", INTEGER, RD\_WR, NULL, 0);i = 0; mibsetvalue(thismib, \&i, 0);mibsetcallback(thismib, get\_dio, set\_dio);
loop()
每当代理检测到数字输入的状态变化时,它将发送一个陷阱。由于 uSNMP 代理不是可重入的,陷阱应该只在主循环中构建和发送。
if ( x & 0x01 ) {  vblistReset(\&response); dInIndex\[17]='0'+y; // 使用响应缓冲区构建陷阱  if ( lastDIN & 0x01 ) { // 输入引脚 y 曾为 1  i = 0; // 现在为 0  vblistAdd(\&response, dInIndex, INTEGER, \&i, 0);  trapBuild(\&request, enterpriseOID, hostIpAddr,   ENTERPRISE\_SPECIFIC, 1, \&response);  }  else {  i = 1;  vblistAdd(\&response, dInIndex, INTEGER, \&i, 0);  trapBuild(\&request, enterpriseOID, hostIpAddr,   ENTERPRISE\_SPECIFIC, 2, \&response);  }  trapSend(\&request, trapDstAddr, TRAP\_DST\_PORT, rwCommunity);}
同样,如果代理在处理 SNMP 请求时遇到不匹配的团体字符串,它会发送一个认证失败陷阱。
if ( processSNMP() == COMM\_STR\_MISMATCH ) {  trapBuild(\&request, enterpriseOID, hostIpAddr, AUTHENTICATE\_FAIL, 0, NULL);  trapSend(\&request, trapDstAddr, TRAP\_DST\_PORT, rwCommunity);}
测试结果
差不多就是这样了。uSNMP 库具有发出 SNMP 请求和处理响应的功能;并包括命令行示例,如 usnmpget 和 usnmpset,用于测试 usnmpd.ino 代理。另一种选择是使用 Net-SNMP 二进制文件。下面显示了两组测试:
使用 uSNMP 工具测试。
使用 Net-SNMP 工具测试。
常见问题
如何发送陷阱?
参见示例程序 usnmpd.ino 和 usnmptrap.c。变量绑定列表可以选择性地解析,以便带有 NULL 数据的变量绑定对将由回调函数填充,很像 Get 操作。
Arduino 和 *nix/Windows 移植版有什么区别?
简而言之,是套接字 API 和 SRAM 大小。有限的 SRAM 限制了数据缓冲区大小和 MIB 树中的条目数量。参见 usnmp.h 和代理示例 usnmpd.c 以及 usnmpd.ino。
如何将 uSNMP 移植到另一个微控制器或操作系统?
平台特定的问题是网络数据包大小和数据缓冲区大小。这些在文件 usnmp.h 中定义。可能需要修改 SnmpAgent.c 和 SnmpMgr.c 中的套接字 API。您不必担心字节序,因为 uSNMP 库已经处理了它。
为什么 uSNMP 库是用 ‘C’ 而不是 ‘C++’ 编写的?
‘C’ 一直并广泛用于嵌入式系统开发。使用 ‘C’ 使 uSNMP 更能适应过去、现在和未来的项目。从 Arduino 移植和示例中可以看出,它在 C++ 环境中也能工作。
有什么注意事项吗?
-
它不是并发代理 —— 它一次处理一个 SNMP 请求。
-
也不是可重入的。例如,不要尝试在回调函数中发送陷阱,因为响应数据包缓冲区也被用作构建陷阱数据包的临时空间,以节省 RAM 空间。相反,在主程序循环中发送陷阱。
-
不支持任意 OID,即所有 OID 字符串必须以三个前缀之一开头:
‘B’ 表示
mgmt:mib2 - ``1.3.6.1``.2.1
‘E’ 表示
experimental - ``1.3.6.1``.3
‘P’ 表示
private:enterprises - ``1.3.6.1``.4.1
例如
mib-2::sysDescr.0
是B.1.1.0
-
BER 编码的 OID 不得超过 127 字节。通常这应该不是问题,因为需要非常长的 OID 才能超过这个限制。
-
在某些整数为 16 位的系统上,如 Arduino UNO,OID 中的任何点分十进制数都不应超过 65535。
uSNMP 支持通过团体字符串进行多路复用吗?
是的,您可以设置一个回调函数来处理请求者的团体字符串,进而可以将其映射到设备名称。此功能对于构建网关以接收和转换来自多个非 SNMP 源的数据非常有用。
如何开始使用?
参见 README_Build.md
许可证
uSNMP 以 BSD 风格的许可证发布。参见 LICENSE
致谢
- M. Tim Jones
(注:文档部分内容可能由 AI 生成)