This is an introduction to the uSNMP library and how to use it to enable low-cost hardware to leverage the power of SNMP for IoT and asset monitoring projects.
uSNMP ("micro-SNMP") is a small and portable 'C' library for developing SNMPv1 agent and manager. Ports to Arduino IDE, Windows and *nix are included in the source code, and have been tested on Arduino compatible (AVR ATmega328p) and Arduino Mega with Ethernet Shield, NodeMCU v0.9 (Expressif ESP8266), Windows (compiled with Embarcadero BCC32C C++ compiler) and Cygwin (with gcc).
How Small Does uSNMP Get?
On an Arduino ATmega328p with an Ethernet Shield, an uSNMP agent that implements the mib-2::system
table, three minimalist tables of 2 digital inputs (with trap sent when the state toggles), 2 digital outputs and 1 analog input, is about 20kB, inclusive of the SPI, Ethernet, UDP, DNS routines. It supports Get
, GetNext
, Set
operations and sends a Trap
when the digital inputs toggle. The 2kB SRAM limits the number of MIB entries and network packet size (and thus request and response length). By forgoing the mib-2::system
table, more digital and analog I/O pins can be added to the respective tables. On an Arduino Mega or ESP8266, bigger buffers and more I/O pins can be supported as the SRAM is far bigger.
What Can uSNMP Do?
The library includes functions to store and traverse a MIB tree in lexigraphical order; support callback functions to get and set value of a MIB leaf node, make SNMPv1 Get
, GetNext
, Set
request; construct and process the response; create and parse a varbind
list, send a Trap
and takes care of Endianness.
Why Use SNMP?
SNMP (Simple Network Management Protocol) is the de-facto standard in IT equipment and well-supported in the industrial and built environment sectors: network equipment, servers and storage, UPS, rectifiers, teleprotection or protection signalling equipment, RTU, remote I/O, etc. Its concept of a Management Information Base (MIB), defined in a text file in ASN.1 notation, is its superpower. MIB files work like a data dictionary or a device description lanaguage. They make it easy to onboard a new device into a SNMP-based management software, of which there are many including open source ones, with features like geographical and topologial map overlay, dashboard, charts, event logs, event action filters, trouble ticketing.
This setup is ideal for IoT applications or asset management, where there are many identical sites and devices with few points. Contrast this with a SCADA/HMI software which is suited for single site with many points, like a process plant or a building, and has highly visual features such as 3D and animation.
Then, Why SNMPv1, and Not SNMPv2 or v3?
The SNMP protocol, despite its name, is really not simple to implement nor fit into small processors, even for SNMPv1. What are you losing with SNMPv1 versus v2/v3? Mainly operations for bulk data query and security features. But consider this: a device's MIB can still be traversed fully with SNMPv1 operations. And most, if not all, industrial protocols including dominant ones like Modbus, BACnet and Profinet, do not have built-in or have weak security features. This is not to trivialise security, but to urge pragmatism when circumstances permit.
How Does uSNMP Work?
The uSNMP library extends the Embedded SNMP Server presented in chapter 8 of the book "TCP/IP Application Layer Protocols for Embedded Systems" by M. Tim Jones (Charles River Media, 2002. ISBN 1-58450-247-9) who very eloquently wrote:
Quote:
"... The problem with SNMP message generation is ... forward (unknown) TLV lengths ... The solution chosen for this problem is to parse the SNMP request using a predictive parser and build the response as we go ... We predictively parse through the SNMP PDU, and when we reach the final TLV, we return through our function call chain and update the length values of the TLVs as needed."
How Do I Use uSNMP?
There are code examples of agent and command line utilities that can be used as templates for developing a SNMPv1 agent, to make a SNMPv1 request and process the response, and to send a trap. The example uSNMP agent usnmpd.c, for Windows and *nix, reads OIDs and value pairs from a file, and can be used as a SNMPv1 gateway by having a poller program formats and writes its received data to this file. Another agent example usnmpd.ino turns an Arduino board into a SNMP-enabled controller with digital and analog I/O. MIB files are in the mibs directory. The ARDUINO.MIB file is for an Arduino Software (IDE) managed board, and the Private Enterprise Number (PEN) is 38644 of Armadino.
usnmpd.ino - A SNMP Agent for Arduino
Let's take a deeper look into usnmpd.ino. For 3rd party hardware packages such as NodeMCU, it is first necessary to add the URL of their Boards Manager JSON file in the Arduino IDE. The URLs point to JSON index files that Arduino IDE uses to build the list of available installed boards. This may be done from File... Preferences and for ESP8266 boards like NodeMCU:
The first few lines of usnmpd.ino sets up the network connection (Ethernet or WiFi), IP address and agent configuration.
IPAddress hostIpAddr( 192, 168, 1, 177 ),
dnsServer( 192, 168, 1, 1 ),
hostGateway( 192, 168, 1, 1 ),
hostNetmask( 255, 255, 255, 0 );
#ifdef ARDUINO_ETHERNET
unsigned char hostMacAddr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
#else // assume ARDUINO_WIFI
char staSSID[] = "Wifi_SSID";
char staPSK[] = "Wifi_Password";
#endif
#define ENTERPRISE_OID "P.38644.30" // used as sysObjectID and in trap
#define RO_COMMUNITY "public"
#define RW_COMMUNITY "private"
#define TRAP_DST_ADDR "192.168.1.170"
uSNMP defines three prefixes for Object ID that every valid OID is required to start with:
B denotes Mgmt-Mib2 - 1.3.6.1.2.1
E denotes Experimental - 1.3.6.1.3
P denotes Private-Enterprises - 1.3.6.1.4.1
Thus, sysDescr.0 (1.3.6.1.2.1.1.1.0)
will be coded as "B.1.1.0
" and Enterprise OID of "1.3.6.1.4.1.38644.30
" as "P.38644.30
".
setup()
initialise the board and agent 'engine', including constructing the MIB tree, and sending a coldStart
trap. Pins D2 to D5 are designed as digital inputs, D6 to D8 are digital outputs, and A0 and A1 are analog inputs, depending on the amount of SRAM available on the target microcontroller. Having included the mib-2::system
table, an Arduino UNO with ATmega328p could have D2, D3, D6, D7 and A0, while a Arduino Mega may go beyond D5, D8 and A1 if one so wishes. Otherwise, omitting the system
table will free up space on an UNO for more pins.
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);
The MIB tree is constructed with the miblistadd()
fucntion, that graft a MIB leave node onto it in lexigraphical order. If needed, this is followed by setting the node's value, and attaching callback functions to respond to SNMP Get
and Set
operations. In the extract below, sysDescr
is assigned a character string that already holds the system description. sysObjectID
is initialised with the EnterpriseOID
after it has been encoded with BER (Basic Encoding Rule). sysUpTime
is set up with get_uptime()
callback to fill in the system uptime whenever a Get
operation is asked.
thismib = miblistadd(mibTree, "B.1.1.0", OCTET_STRING, RD_ONLY,
sysDescr, strlen(sysDescr));
thismib = miblistadd(mibTree, "B.1.2.0", OBJECT_IDENTIFIER, RD_ONLY,
entOIDBer, 0); i = str2ber(enterpriseOID, entOIDBer);
mibsetvalue(thismib, (void *) entOIDBer, (int) i);
thismib = miblistadd(mibTree, "B.1.3.0", TIMETICKS, RD_ONLY, NULL, 0);
i = 0; mibsetvalue(thismib, &i, 0);
mibsetcallback(thismib, get_uptime, NULL);
The digital and analog I/O pins are presented in SNMP tables. To save memory, these tables are minimalist, comprising only an index and the pin value. Callback functions are required so that the values are retieved just-in-time when responding to a Get
or Set
request. Thus, for digital output D6 for instance:
thismib = miblistadd(mibTree, "P.38644.30.2.1.1.6", INTEGER, RD_ONLY, NULL, 0);
i = 6; mibsetvalue(thismib, &i, 0);
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);
The agent is designed to send a trap whenever it detects a change of state in a digital input. As the uSNMP agent is not re-entrant, trap should only be built and send in the main loop.
if ( x & 0x01 ) {
vblistReset(&response); dInIndex[17]='0'+y; if ( lastDIN & 0x01 ) { i = 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);
}
Likewise, if the agent encounters a mismatched community string in processing a SNMP request, it sends an Authentication Failure trap.
if ( processSNMP() == COMM_STR_MISMATCH ) {
trapBuild(&request, enterpriseOID, hostIpAddr, AUTHENTICATE_FAIL, 0, NULL);
trapSend(&request, trapDstAddr, TRAP_DST_PORT, rwCommunity);
}
Test Results
That's just about it. The uSNMP library has functions to make SNMP request and process response; and includes command line examples such as usnmpget
and usnmpset
which are used to test the usnmpd.imo
agent. The alternative is to use the Net-SNMP binaries. Both set of tests are demonstrated below: