Implement a secure ICS protocol targeting LoRa Node151 microcontroller for controlling irrigation.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
John-Mark Gurney b3ec53c4cb drop debug print 2 years ago
loramac add pin description, and use the analog pin to provide randomness.. 3 years ago
stm32 use funopen/stdio to buffer output, "fix" for FreeBSD... 2 years ago
strobe add the start of the C version for the uC... 3 years ago
.gitignore make sure the irrigation key file is ignored... 3 years ago
LICENSE.txt talk about the special ST license.. 3 years ago
Makefile use python3 to run python 3 programs... Using python is a mistake 2 years ago add notes about unix dgrams.. 3 years ago add support for replying last message if it was lost.. 3 years ago use python3, note how to run w/ FreeBSD, and code quality... 2 years ago
arch.getxt add some diagrams, and the infrastructure to make them... 3 years ago
board-config.h get tx/rx working between two Node151's.. 3 years ago
board.c missed setting up routing for the analog pin.. 3 years ago add some diagrams, and the infrastructure to make them... 3 years ago
comms.c make sure a short packet doesn't cause an assert failure... 3 years ago
comms.h add support for starting a new session... 3 years ago
conns.getxt add some diagrams, and the infrastructure to make them... 3 years ago
irr_main.c if pkt too large, ignore, restart rx on err/timeout, loop processing schedule.. 3 years ago make sure a short packet doesn't cause an assert failure... 3 years ago support importing this w/o the lib... 3 years ago make sure we don't buffer the serial port, we need the packets to 3 years ago
main.c use funopen/stdio to buffer output, "fix" for FreeBSD... 2 years ago
misc.c use funopen/stdio to buffer output, "fix" for FreeBSD... 2 years ago
misc.h add memory debug buffer.. Fix FreeBSD by adding a delay.. 2 years ago drop debug print 2 years ago
requirements.txt add the required python packages.. 3 years ago
strobe_rng_init.c use sbrk(0) instead of end to get the uninitalized memory... 3 years ago
strobe_rng_init.h add support for saving PRNG state to EEPROM... 3 years ago
sysIrqHandlers.h get tx/rx working between two Node151's.. 3 years ago forgot to add the copyright for this file.. 3 years ago change the default to be func.. 3 years ago

LoRa Irrigation System

This project is to build an irrigation system from LoRa capable microcontrollers. The Heltec Node151 was chosen due to it’s small size and inexpensive cost.

Design Decisions

While investigating this project, the LoraWAN protocol was investigated, but after looking at the code complexity and other operational requirements, if was decided that for this project, it was safer to target a direct Node to Node style communication system. This would allow the implementation to be more simple, and security to be built in (LoRaWAN does have a crypto layer, BUT, trusting/auditing it and any library that implements it would be a larger task than I want to undertake). It could also be used for other projects that need security.

One of the other requirements is that the code be 100% open sourced, not GPL licensed, and no proprietary components. This meant that using IDE’s like ST’s STM32CubeIDE which is only available in binary form was not a choice, as that would preclude building on an operating system other than Windows/MacOSX/Linux.


There are a number of components to make this system work. The overall flow is:

*                                                                                              *
* +---------+              +-------------+            +-------------+         +--------------+ *
* | |  multicast   | |  USB VCP   | |  LoRa   | lora.irr.elf | *
* |         +--------------+             +------------+   main.c    +---------+  irr_main.c  | *
* +---------+              +-------------+            +-------------+         +--------------+ *
*                                                                                              *

The component is the front end/UI that is used to send commands to controller. This program establishes a secure communications channel to the controller. The controller’s firmware is in lora.irr.elf, and the main source file is irr_main.c.

The middle to components, and are used to pass messages between the former two. The program takes multicast datagrams that are received on, which are with out any framing, prepends pkt:, hex encodes the data and terminated w/ the new line character, and sends them via the USB VCP provided by The gateway firmware then decodes the packet and transmits it via the LoRa radio to the irrigation controller. Any received packet is returned similarly, but this time with data: prepended, for to multicast back to

The reason no particular framing is required for addressing or destination is that the protocol is secure, and only the party that is able to decrypt the proper packets will be accepted, and any invalid packets will be ignored.


The build system uses the BSD flavor of make. This is the default make on the BSDs, originally called pmake, but also available as bsdmake for MacOSX, and available as bmake.

It also depends upon ARM’s GNU Arm Embedded Toolchain, which uses gcc as the compiler. It would be good to get it cross-compile with clang as well, but that requires finding a libc like the nano libc that is provided by the toolchain.

One of the required parameters of the build is the shared key used for authentication. A random key can be made using the command: make irrigation_key, or it can be provided via the make command by setting the variable IRR_KEY.

Note: Both IRR_KEY and the argument to will encode the provided key to UTF-8.

Once ARM’s toolchain is in your path, the following should work:

export MAKEOBJDIR=build
bsdmake all IRR_KEY=<sharedkey>

And in the directory build, two files, lora.irr.elf and should be present. The file lora.irr.elf should be flashed on the Node151 device that is used for interfacing to the irrigation system as described in Deploying. The file should be used on another Node151 that will be attached to a computer used as the gateway which runs the software as described in Using.


Flashing can be done via the open source tool OpenOCD. For this, I use a Digilent HS1 JTAG programmer utilizing the resistor hack to allow an FTDI JTAG programmer to control the bi-directional SWIO pin.

One caveat w/ MacOSX, is that it may be necessary to unload the kext via the command:

sudo kextunload -b

as OpenOCD wants direct access to the FTDI driver.

Once that happens, the device can be programmed using the following command:

sudo openocd -f interface/ftdi/digilent-hs1.cfg -f interface/ftdi/swd-resistor-hack.cfg -f target/stm32l1.cfg -c "init" -c "reset init" -c "program build/lora.irr.elf verify reset exit"


The pinout guide for the Node151.

The default pins PB5-7,9 are used as active low controls for the relays. They are mapped to channels 0 through 3 respectively. The LED on PB8 is mapped to channel 4. This is useful for testing if a command works or not.

The pin PB15 is used as an analog input for an RNG source. This pin should be grounded.


Here is a diagram of the connections:

*                                                                                         *
*                                   GND                                                   *
*                     +--------------------------------------------------+                *
*                     |                                                  |                *
* +--------+  ~/~   +-+-----------+  GND           +---------+  GPIO   +-+--------------+ *
* | 24V AC +--------+             +----------------+ Node151 +---------+     Relay      | *
* +--------+        |             |                +-+-------+         +-+--------------+ *
*                   |             |  +5V -> VUSB     |                   |                *
*                   | AC-DC PS 5V +------------------+                   | +5V -> JD-VCC  *
*                   |             |                                      |                *
*                   |             |                                      |                *
*                   |             +--------------------------------------+                *
*                   +-------------+                                                       *
*                                                                                         *

The normal supply used for irrigation values is 24V AC. This means an additional power supply is needed to convert to the 5V supply that is used by the Node151. Make sure this is well filtered as both the relays on the board (talked about below), and the irrigation valves will cause significant noise. The first PS I made was a simple DC-DC buck converter + a full wave rectifier which, while alone was enough to power the uC, was not enough when the relays were actuated, and even when a little bit of filtering was added after the rectifier (22uF), enough to keep it happy w/ the relays, it was not enough when the irrigation valves actuated.

In order to control the values, a relay board, similar to this one, can be used. Despite the GPIO on the Node151 being 3.3V, and the relays requiring 5V, the jumper on the right side, VCC-JD-VCC, can be removed to allow dual voltage operation. The GND on the input/VCC pinout actually belongs to the relay power via JD-VCC, and NOT for the VCC->INx pins, despite them being next to each other. The GND and JD-VCC should be connected to the 5V power supply, while VCC is connected to VDD on the Node151, and INx pins to the respective GPIO pins.


The script requires at least Python 3.8. It also uses the strobe library that in distributed in this repo. In general a virtualenv is recommended for all installed Python software to prevent version conflicts, but is not always necessary. The requirements.txt file contains the necessary modules to be installed, but simply adding the directory strobe/python to PYTHONPATH should be sufficient.

The program takes a single argument, which is the device file for the VCP that runs on the gateway. In my case, the device name is /dev/cu.usbmodem1451 as I am on my MacBook Pro, so the command to launch the gateway is simply:

python3 /dev/cu.usbmodem1451

Note: On FreeBSD, the default open mode echos characters back to the gateway causing it not to work. It is advised to setup the modem tty via the command:

stty -f /dev/cuaU0.init gfmt1:cflag=cb00:iflag=2e02:lflag=43:oflag=2:discard=f:dsusp=19:eof=4:eol=ff:eol2=ff:erase=7f:erase2=8:intr=3:kill=15:lnext=16:min=1:quit=1c:reprint=12:start=11:status=14:stop=13:susp=1a:time=0:werase=17:ispeed=9600:ospeed=9600

before running the command. Replace /dev/cuaU0.init with the respective device. The above was generated by running cu -l /dev/cuaU0 in one terminal, while obtaining the info via stty -g -f /dev/cuaU0 in another terminal.

Once that is running, then the program’s multicast packets will be forwarded out via the LoRa radio.

To test it, a simple ping command can be used, or turning on or off the on board LED via channel 4 using the setunset command. The ping command:

python3 -s <sharedkey> ping

To turn off the LED (which defaults to on):

python3 -s <sharedkey> setunset 4 0

Either of these commands should exit w/o message or error.

Multiple commands may be specified by separating them w/ -- (two hyphens). For example:

python3 -s <sharedkey> -- setunset 4 0 -- setunset 1 1

The first -- is required to denote the end of option parsing, otherwise the second -- will be used, and the first setunset command will be tried to be parsed as an argument.

If you have regular set of commands to run, they can be stored in a file. Each line will be a single command, so you can have a file similar to:

waitfor 5000
setunset 0 0
setunset 1 0
setunset 2 0
setunset 3 0
runfor 60000 0
waitfor 2000
runfor 60000 1
waitfor 2000
runfor 60000 2
waitfor 2000
runfor 60000 3

which will make sure all the valves are turned off, then run each one in succession for 60 seconds, with a 2 second wait between.

There are two types of commands, ones that execute immediately, and ones that are queued up for future execution. The immediate commands are:

  • adv: Sets the current executing command to 0. An optional argument specifies how many commands to advance by. Note that only the time they are run is set to zero, so channels will be very briefly activated. See the clear command to avoid this.
  • clear: No argument, removes any future commands. Current command remains executing. To clear all commands and stop all operations, first do a clear, followed by an adv.
  • ping: No argument, used to verify communication works.
  • setunset: First argument is channel, second argument is 0 or 1, specifying to turn the channel off (0) or on (1).

The following commands are queued. The first argument is the number of milliseconds to run the command for before advancing to the next command. The available commands are:

  • runfor: And additional argument specifies the channel. The channel will be set to on, and then when the command completes, The channel will be set to off.
  • waitfor: Wait for the specified time before advancing to the next enqueued command.


Significant portions of this code is copied from various reference implementations. Basic review shows that it is of questionable quality. It would be good to fully review all the code under stm32 and loramac for any bugs and other issues. The buffer handling has already been improved, but there is still plety of work that can be done to improve it further.