Inside impOS’ LAN Functionality
impOS™ is able to make and maintain connections to other devices that are part of the local area network to which the imp is connected. These are links established at the LAN level — there is no longer any need to communicate with local devices via the Internet and the imp’s agent, as was the case with earlier versions of impOS (though you may continue to do so if you prefer).
This functionality is intentionally limited, to ensure that imp security is not compromised. Current restrictions include:
Within these restrictions, however, your application has considerable scope to connect to devices on the local network and exchange data with them.
Note The local networking functionality described in this document makes use of impOS’ network interface management system, in particular its standard Network Interface Identifiers (NIIs), which can be used to prepare specified imp network interfaces for local area networking. Please see the separate document, imp Network Interface Management, for more information on NIIs and a detailed description of impOS’ network interface management system.
To initiate a local network connection, your code calls the new imp API method imp.net.open() and passes in a standard NII, such as "eth0"
, "wl0"
or "cell0"
, which tells impOS which interface you wish to use for the connection. This call powers up (if necessary) and readies the named interface for use, provided it is available. If the specified interface is not available (you might have disabled it using impOS’ network interface management system, eg. via imp.net.setserverinterfaces()), then imp.net.open() will throw an exception.
If the chosen interface is available to be used, imp.net.open() immediately returns a reference to an object that represents it. When imp.net.open() returns, the interface may not yet be connected, so you should use a callback function (registered with imp.net.open()) to discover when the chosen interface is connected so that you can use it for local networking (see below).
The connection-state notification callback passed into imp.net.open() via its second parameter is a function which has one parameter of its own, state, which receives an integer connection status code. The possible values which may be passed into state are listed below:
impOS Constant | Value | Network Interface State |
---|---|---|
imp.net.UNKNOWN | -1 | Idle/unknown |
imp.net.STARTING | 0 | Starting |
imp.net.CONNECTED | 1 | Connected |
imp.net.WIFI_SCANNING | 100 | WiFi scanning |
imp.net.WIFI_JOINING | 101 | WiFi joining |
imp.net.WIFI_WPSING | 102 | WiFi WPS in progress |
imp.net.WIFI_LINK_UP | 103 | WiFi link up |
imp.net.WIFI_DHCPING | 104 | WiFi getting IP address via DHCP |
imp.net.WIFI_STOPPED | 105 | WiFi stopped |
imp.net.WIFI_STOPPED_UNHAPPY | 106 | WiFi sub-system unhappy |
imp.net.ETHERNET_LINK_UP | 200 | Ethernet link up |
imp.net.ETHERNET_DHCPING | 201 | Ethernet getting IP address via DHCP |
imp.net.ETHERNET_STOPPED | 202 | Ethernet stopped |
imp.net.ETHERNET_STOPPED_UNHAPPY | 203 | Ethernet sub-system unhappy |
imp.net.ETHERNET_STOPPED_NO_LINK | 204 | Ethernet stopped: No link |
imp.net.CELLULAR_PINGING | 300 | Cellular contacting modem |
imp.net.CELLULAR_WAITING_FOR_SIM | 301 | Cellular waiting for SIM |
imp.net.CELLULAR_PPP_CONNECTING | 302 | Cellular connecting via PPP |
imp.net.CELLULAR_REGISTERING | 303 | Cellular registering with network |
imp.net.CELLULAR_REGISTRATION_DENIED | 304 | Cellular network registration request denied |
imp.net.CELLULAR_STOPPED | 305 | Cellular stopped |
imp.net.CELLULAR_STOPPED_UNHAPPY | 306 | Cellular modem unhappy |
Electric Imp reserves the right to alter the value of these constants in future impOS releases.
You use the interface object returned by imp.net.open() to open a local network connection once the callback function registered with imp.net.open() indicates that the interface is connected (see above). You can also use the interface.getstate() method to check whether the interface is connected; it returns the same integer passed into the imp.net.open() callback and is the only way to acquire connection state information if you’re not using a callback. imp.net.open() does not block, but returns immediately.
impOS currently supports only UDP communication on local connections. UDP sockets are initiated by calling the new imp API method openudp() on an interface object returned by imp.net.open(). If you wish, you can pass a function into interface.openudp() which will be called when data is received via the UDP socket. This callback has the following parameters of its own:
"192.168.0.1"
."192.168.0.2"
.When calling interface.openudp(), you can also set the local port number to which UDP traffic will be bound. You pass this value as an integer into interface.openudp()’s second parameter. You must bind the network interface to a port, either manually here or when you send data by call the send() method on the udpsocket object returned by interface.openudp(). If you don’t specify a port number, impOS will set one for you when you call udpsocket.send() — it will be returned to you via the data-received callback’s port parameter.
interface.openudp() returns an object representing the UDP socket created by impOS. This object can now be used to send data out. This, and the other methods discussed above, are demonstrated in the following simple example:
local udpsocket;
local interface;
local ucastIP;
local bcastIP;
function dataReceived(fromAddress, fromPort, toAddress, toPort, data) {
// Check that we are the destination (unicast or broadcast)
if (toAddress == ucastIP || toAddress == bcastIP) {
// Log the receipt of data
server.log("Received " + data.len() + "bytes from " + format("%s:%d", fromAddress, fromPort));
// Echo it back
local result = udpsocket.send(fromAddress, fromPort, data);
if (result != 0) server.error("Could not send the data (code: " + result + ")");
}
}
function interfaceHandler(state) {
if (state == imp.net.CONNECTED && udpsocket == null) {
// We're connected, so initiate UDP
udpsocket = interface.openudp(dataReceived, 2001);
}
if (state == imp.net.ETHERNET_STOPPED ||
state == imp.net.ETHERNET_STOPPED_UNHAPPY ||
state == imp.net.ETHERNET_STOPPED_NO_LINK ) {
server.error("Ethernet error - closing UDP");
if (udpsocket != null) {
udpsocket.close();
udpsocket = null;
}
}
}
// Set up a UDP echo server
// Get an interface object
interface = imp.net.open({"interface":"eth0"}, interfaceHandler);
// Get the imp's IP and the network broadcast IP
local i = imp.net.info();
ucastIP = i.ipv4.address;
bcastIP = i.ipv4.broadcast;
The first call, to imp.net.open(), instantiates an interface object representing the host imp’s primary Ethernet interface — we assume this code is called on an imp005. The call also takes an optional reference an interface state-change notification callback; here we provide a reference to the function interfaceHandler() for this purpose.
The callback checks the value passed into its state parameter to see if the interface is connected, ie. that it’s ready for local networking. If it is, the code calls the target interface object’s openudp() method to open a UDP connection on port 2001 (the second argument). The interface object is referenced by the variable interface. We also provide a reference to the data-received callback function, dataReceived(), which will handle data received via UDP. The openudp() method returns a udpsocket object which we retain as udpsocket and make use of in dataReceived().
In addition to data, dataReceived() also receives the IPv4 address of the source and the port number; it logs this information. But it only does so if the IP address of the datagram’s destination is the imp’s own, or is a broadcast packet (ie. intended for all the devices on the network). If the packet is relevant to this imp, it uses the udpsocket.send() method to transmit the received data back to the source.
As the above example shows, the send() method, called on a udpsocket object returned by interface.openudp(), has three parameters:
The supplied address must be formatted correctly as a dotted quad string and it must not be an Internet routable address. In other words, it must only be a local/private in the ranges 192.168.0.0—192.168.255.255, 10.0.0.0—10.255.255.255, or 172.16.0.0—172.31.255.255). Incorrectly formatted addresses and/or address outside of these ranges will trigger a runtime error and the UDP socket will be closed.
udpsocket.send() returns an integer value which represents the status of the operation. Successful transmissions are signalled by the value 0 — all other values represent a non-throwing error. Possible error values are listed below.
Error Code Constant | Value | Description |
---|---|---|
SEND_ERROR_NOT_CONNECTED | 1 | Could not connect to the destination machine |
SEND_ERROR_WOULDBLOCK | 4 | The transmission was rate-limited (see below) |
Outgoing UDP packets are rate limited, to protect the local network and impOS connectivity. You can send up to 100 packets simultaneously in burst mode, or 25 packets a second continuously. In other words, an initial allowance of 100 packets is replenished back up to 100 at a rate of 25 per second.
To send data to all UDP devices on the network via the set port, transmit to the network’s broadcast address. Data sent to other IP address will be sent specifically to the device with that address.
Note You may choose to determine the broadcast address of the local network. If you use the standard proxy for this value, 255.255.255.255, impOS will emit this value unchanged, leaving it to the router to convert to the correct address for the local network.
As shown in the example above, UDP data is passed to the application via the data-received callback function that you register when calling interface.openudp(). This callback’s third parameter receives the incoming data as its argument.
The function receives the address and port from which the datagram was sent. It also receives the destination address and port, allowing your code to determine whether it is the intended recipient. This will be the case if the destination address is either that of the code’s host imp, or destination address is the network’s broadcast address. Both of these addresses can be gathered using imp.net.info() as the example above shows.
If you did not provide a data-received callback via interface.openudp(), or you wish to switch to an alternative callback at any time, you can register a data-received callback by calling udpsocket.onreceive().
Note If you register a new callback immediately after sending data (using udpsocket.send()), there is a small window during which packets could be dropped. For this reason, you should either call udpsocket.onreceive() before calling udpsocket.send(), or specify the receive callback and local port number in interface.openudp().
If you provide extended configuration information in a call to imp.net.open(), ie. you include any of the ...config keys in the NIC you pass in, impOS will not retain this information in persistent storage. If the connection attempt fails, or if the attempt to connect is successful but the connection is subsequently lost for any reason at all, including Squirrel virtual machine restarts, device power-cycles, and manual and unexpected disconnections, then the supplied configuration will be discarded and will not be used by impOS in any further connection attempts.
This is also the case the interface object returned by a call to imp.net.open() goes out of scope. Additionally, any attempt to connect using server.connect() that is made while the imp is still processing the imp.net.open() call (which takes place asynchronously) will cause the extended NIC to be discarded and whichever configuration has been written to persistent storage, eg. by BlinkUp™ or calls to the imp API methods imp.setwificonfiguration(), imp.setstaticnetworkconfiguration() or imp.setproxy(), to be used in its place for both the connection to the server and to the local network.
If, however, you call imp.net.open() and simply name an interface in your NIC, eg. {"interface" : "wl0"}
, then impOS will use that interface for all future connections, or until an alternative interface is specified. Interfaces that required extra information to connect — for example, WiFi connections may require an SSID and password to be supplied — will make use of persisted configuration data in this instance. If there is no relevant configuration data available, the connection attempt will fail.
It’s important to understand that an imp’s local network connections are entirely separate from its connection to the server (ie. to its agent in the impCloud and thus the wider Internet), even if both operate across the same network interface.
Your application can be notified of unplanned server connection losses by registering a callback function using the imp API method server.onunexpecteddisconnect(). UDP connection losses will be reported via the callback you register with imp.net.open(). These two callbacks are entirely separate, and they will only be called at the same time if, for example, the local network goes down, breaking an imp’s connections to other devices and to the server.
It is entirely possible that though the local network continues to function, that network’s Internet link has been lost. In this case, your application can continue communicating with other local devices while it attempts to reconnect to the server, perhaps using a different network interface.
Equally, you can call server.disconnect() to break the server connection manually and be sure that though the imp is offline — ie. not connected to the impCloud — it will still be connected to other, nearby devices via the local network and UDP.
This mechanism provides a means for an imp to interact with multiple devices that are using the same local network that as is the imp, using UDP. Your code will still need to encode/decode the data passing between connected devices and perform device discovery, eg. by sending a broadcast UDP packet.
The following example might be run on an imp-based sensor hub. At start-up it sets up UDP communications and sends a broadcast to all the devices on the local WiFi network. Only our application’s sensor nodes should respond; when they do, the code retains their IP addresses for future use.
Every 60 seconds, the hub sends a data-request packet to each known sensor node; the responses, when received, are transferred to the agent, perhaps for relaying to a cloud-hosted data analysis platform. The results are made available via the log and on a web UI served by the hub’s agent.
The hub code will run on any imp-based breakout board. The sensor-node code is intended to be run on impExplorer™ Kits.
Note UDP makes no guarantees about the order in which packets will be received — or even whether they will be received at all. As such, you should expect to see some devices log Bad Sequence Number errors when, for example, a data packet was sent by a node but not received by the hub; in this case, when the hub next requests a reading, the sequence number it sends will be lower than the node expects, and the request will be ignored. Subsequent requests from the hub will be correctly sequenced, however.
Note There is no Node Agent Code in this example.