Package: riotctrl
This provides python object abstraction of a RIOT device.
The first goal is to be the starting point for the serial abstraction and
build on top of that to provide higher level abstraction like over the shell.It could provide an RPC interface to a device in Python over the serial port
and maybe also over network.The goal is here to be test environment agnostic and be usable in any test
framework and also without it.TestingRuntoxto run the whole test suite:tox
...
________________________________ summary ________________________________
  test: commands succeeded
  lint: commands succeeded
  flake8: commands succeeded
  congratulations :)UsageRIOTCtrl provides a python object abstraction of a RIOT device. It’s
meant as a starting point for any serial abstraction on which higher
level abstractions (like a shell) can be built.fromriotctrl.ctrlimportRIOTCtrlenv={'BOARD':'native'}# if not running from the application directory the a path must be providedctrl=RIOTCtrl(env=env,application_directory='.')# flash the applicationctrl.make_run(['flash'])# run the terminal through a contextmanagerwithctrl.run_term():ctrl.term.expect('>')# wait for shell to startctrl.term.sendline("help")# send the help commandctrl.term.expect('>')# wait for the command result to finnishprint(ctrl.term.before)# print the command result# run without a contextmanagerctrl.start_term()# start a serial terminalctrl.term.sendline("help")# send the help commandctrl.term.expect('>')# wait for the command result to finnishprint(ctrl.term.before)# print the command resultctrl.stop_term()# close the terminalCreating a RIOTCtrl object is done via environments. If empty then all
configuration will come from the target application makefile. But any
Make environment variable can be overridden, for example settingBOARDto a targetBOARDwhich is not the default for that
application.Any make target used on RIOT devices can be used on the abstraction
like:make flash=>ctrl.make_run(['flash']).ctrl.start_term()(make term’s alter ego) by default spawns apexpectchild application. From there interactions with the application
under use can be atomized. In the example below the output of the"help"command is captured:ShellInteractionsRIOTCtrl provides a minimal extensions by using:pexpect replwrap“[A] Generic wrapper for read-eval-print-loops, a.k.a. interactive shells”.
This implements a nice wrapper for RIOT shell commands since it will wait for a
command to finish before returning its output.RIOT already provides aShellInteractionfor the"help"command as well
as many others. To make importing them asfrom riotctrl_shell.sys import Helppossible RIOT’spythonlibsneeds to be part of thePYTHONPATH, this can be done by setting in the environmentPYTHONPATH=$PYTHONPATH:${RIOTBASE}/dist/pythonlibsor doing so in the
scriptsys.path.append('/path/to/RIOTBASE/dist/pythonlibs')The previous example can be re-written usingShellInteraction:fromriotctrl.ctrlimportRIOTCtrlfromriotctrl.shellimportShellInteractionenv={'BOARD':'native'}# if not running from the application directory the a path must be providedctrl=RIOTCtrl(env=env,application_directory='.')# flash the applicationctrl.flash()# alias for ctrl.make_run(['flash'])# shell interaction instanceshell=ShellInteraction(ctrl)shell.start_term()# start a serial terminalprint(shell.cmd("help"))# print the command resultshell.stop_term()# close the terminalor using the already providedHelpShellInteraction:fromriotctrl.ctrlimportRIOTCtrlfromriotctrl_shell.sysimportHelpenv={'BOARD':'native'}# if not running from the application directory the a path must be providedctrl=RIOTCtrl(env=env,application_directory='.')# flash the applicationctrl.flash()# alias for ctrl.make_run(['flash'])# shell interaction instance, Help uses the @ShellInteraction.check_term# decorator, it will start the terminal if its not yet running, and close# it after the command endsshell=Help(ctrl)# create ShellInteractionprint(shell.help())# print the command resultWriting ShellInteractionLets use this simple C shell application as an example:#include<stdio.h>#include<stdlib.h>#include"shell.h"staticunsignedintcounter=0;staticint_cmd_counter(intargc,char**argv){if(argc==1){printf("counter: %d\n",counter);}elseif(argc==2){counter+=atoi(argv[1]);}else{puts("Usage: counter [value]");return-1;}return0;}staticconstshell_command_tshell_commands[]={{"counter","prints current counter or adds input",_cmd_counter},{NULL,NULL,NULL}};intmain(void){charline_buf[SHELL_DEFAULT_BUFSIZE];shell_run(shell_commands,line_buf,SHELL_DEFAULT_BUFSIZE);return0;}This simple command allows to return the current counter value or modifying
by adding a value to it.main(): This is RIOT! (Version: 2021.10-devel-645-g2c3266-pr_kconfig_mtd)
> boardinfo
board: native
cpu: native
> counter 5
> counter -3
> counter
counter: 2AShellInteractionfor this could look as follows:fromriotctrl.shellimportShellInteractionclassCounterCmdShell(ShellInteraction):@ShellInteraction.check_termdefcounter_cmd(self,args=None,timeout=-1,async_=False):cmd="counter"ifargsisnotNone:cmd+="{args}".format(args=" ".join(str(a)forainargs))returnself.cmd(cmd,timeout=timeout,async_=False)Parsing Interaction ResultsParsers can be written for the result of ShellInteraction commands,
these can then be returned in any format, for this a base class
ShellInteractionParser is provided where theparse()method needs to
be implemented.An examples for thecountercommandimportrefromriotctrl.shellimportShellInteractionParserclassCounterCmdShellParser(ShellInteractionParser):pattern=re.compile(r"counter: (?P<counter>\d+)$")defparse(self,cmd_output):devices=Noneforlineincmd_output.splitlines():m=self.pattern.search(line)ifmisnotNone:returnm.group["counter"]env={'BOARD':'native'}# if not running from the application directory the a path must be providedctrl=RIOTCtrl(env=env,application_directory='.')# flash the applicationctrl.flash()# alias for ctrl.make_run(['flash'])# shell interaction instanceshell=CounterCmdShell(ctrl)withctrl.run_term():parser=CounterCmdShellParser()counter=parse.parse(shell_counter_cmd())shell.counter_cmd(4)assertcounter+4=parse.parse(shell_counter_cmd())Interacting with multiple RIOT devicesRIOTCtrl only wrap’s a single RIOT device, handling multiple devices is
not yet handled in RIOTCtrl, but through different environments multiple
RIOT devices can be created and controlled.Users of RIOT andFIT IoT-LABmay have
already ran experiments on multiple ctrls of the same type (e.g:iotlab-m3) using theIOTLAB_NODEmake environment variable. With
this one can easily control which device it is targeting.But if running this locally, with e.g.: multiplesamr21-xproconnected the serial orDEBUG_ADAPTER_IDmust be used to flash the
correct device, and for someBOARDs also the serial portPORT.
These variables can be appended to the environment of the spawned
object, e.g:FIT IoT-LAB:# first device using dwm1001-1 on the saclay siteenv1={'BOARD':'dwm10001','IOTLAB_NODE':'dwm1001-1.saclay.iot-lab.info'}ctrl1=RIOTCtrl(env=env1,application_directory='.')# second device using dwm1001-2 on the saclay siteenv2={'BOARD':'dwm10001','IOTLAB_NODE':'dwm1001-2.saclay.iot-lab.info'}ctrl2=RIOTCtrl(env=env2,application_directory='.')locally:# first samr21-xproenv1={'BOARD':'samr21-xpro','DEBUG_ADAPTER_ID':'ATML2127031800004957'}ctrl1=RIOTCtrl(env=env1,application_directory='.')# second samr21-xproenv2={'BOARD':'samr21-xpro','DEBUG_ADAPTER_ID':'ATML2127031800011458'}ctrl2=RIOTCtrl(env=env2,application_directory='.')For the advanced user one could also do as suggested inmultiple-boards-udevand use an easy to remember variable to identify BOARDs (which would
allow also running the same python code on different setups), if
following the above guide:# first samr21-xproenv1={'BOARD':'samr21-xpro','BOARD_NUM':0}ctrl1=RIOTCtrl(env=env1,application_directory='.')# second samr21-xproenv2={'BOARD':'samr21-xpro','BOARD_NUM':1}ctrl2=RIOTCtrl(env=env2,application_directory='.')FactoriesThe same tasks are done multiple times creating the object flashing it,
starting the terminal and making sure its clean up. Once experiments
grow and take over multiple ctrls this can become tedious, using a
Factory together with a context manager can help with this.Going back to our example lets write a factory inheriting fromRIOTCtrlBoardFactoryBase(or directly fromRIOTCtrlFactoryBasebase class).fromcontextlibimportContextDecoratorfromriotctrl.ctrlimportRIOTCtrl,RIOTCtrlBoardFactoryfromriotctrl_ctrlimportnativeclassRIOTCtrlAppFactory(RIOTCtrlBoardFactory,ContextDecorator):def__init__(self):super().__init__(board_cls={'native':native.NativeRIOTCtrl,})self.ctrl_list=list()def__enter__(self):returnselfdef__exit__(self,*exc):forctrlinself.ctrl_list:ctrl.stop_term()defget_ctrl(self,application_directory='.',env=None):# retrieve a RIOTCtrl Objectctrl=super().get_ctrl(env=env,application_directory=application_directory)# append ctrl to listself.ctrl_list.append(ctrl)# flash and start terminalctrl.flash()ctrl.start_term()# return ctrl with started terminalreturnctrlAnd the script itself can be re-written as:withRIOTCtrlAppFactory()asfactory:env={'BOARD':'native'}ctrl=factory.get_ctrl(env=env)shell=SaulShell(ctrl)parser=SaulShellCmdParser()print(parser.parse(shell.saul_cmd()))GNRC Networking example nativeLets put all the above into practice and script an experiment verifying
connectivity between two ctrls, here multiplenativeinstance will
be used.First create two tap interfaces connected through a bridge interface,
e.g. on linux:iplinkaddnametapbr0typebridgeiplinksettapbr0upiptuntapadddevtap0modetapuser$USERiptuntapadddevtap1modetapuser$USERiplinksetdevtap0mastertapbr0iplinksetdevtap1mastertapbr0iplinksetdevtap0upiplinksetdevtap1upThen we can ping and parse the results asserting than packet loss is
under a threshold or that an mount of responses was received..fromriotctrl_shell.gnrcimportGNRCICMPv6Echo,GNRCICMPv6EchoParserfromriotctrl_shell.netifimportIfconfigclassShell(ifconfig,GNRCICMPv6Echo):passwithRIOTCtrlAppFactory()asfactory:# Create two native instances, specifying the tap interfacenative_0=factory.get_ctrl(env={'BOARD':'native','PORT':'tap0'})native_1=factory.get_ctrl(env={'BOARD':'native','PORT':'tap1'})# `NativeRIOTCtrl` allows for `make reset` with `native`native_0.reset()native_1.reset()# Perform a multicast ping and parse resultspinger=Shell(native_0)parser=GNRCICMPv6EchoParser()result=parser.parse(pinger.ping6("ff02::1# assert packetloss is under 10%"))assertresult['stats']['packet_loss']<10# assert at least one responderassertresult['stats']['rx']>0A more complex example can be seen in the Release Tests:04-single-hop-6lowpan-icmpExamplespytest:ReleaseSpecsunittests:tests/turo,tests/congure_testDiscussionRIOTCtrl base class is not tied into having a serial based interaction, its
the most common usage so far but a new interface orInteractioncould
use different different transports (e.g. COAP), and does not need to provide
a CLI type interface.Test applications could also use Structured Output, like RIOT’sturo,
and in this case parsing CBOR/JSON/XML output could be close to a NOP.