{"id":20415430,"url":"https://github.com/riot-os/riotctrl","last_synced_at":"2025-04-12T17:06:30.755Z","repository":{"id":45563699,"uuid":"272996199","full_name":"RIOT-OS/riotctrl","owner":"RIOT-OS","description":"RIOT Ctrl - A RIOT node python abstraction","archived":false,"fork":false,"pushed_at":"2024-08-12T10:18:01.000Z","size":67,"stargazers_count":13,"open_issues_count":2,"forks_count":8,"subscribers_count":28,"default_branch":"master","last_synced_at":"2025-04-12T17:05:40.046Z","etag":null,"topics":["riot","riot-os","testing-framework","testing-tools"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RIOT-OS.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-06-17T14:29:32.000Z","updated_at":"2024-08-12T10:18:05.000Z","dependencies_parsed_at":"2024-07-23T15:06:15.006Z","dependency_job_id":"96696d35-0bf5-4a98-8f64-b73d6d2669bd","html_url":"https://github.com/RIOT-OS/riotctrl","commit_stats":{"total_commits":65,"total_committers":7,"mean_commits":9.285714285714286,"dds":"0.49230769230769234","last_synced_commit":"4d30bb4ce07493117d3572f40f02f2d3c73ad5c8"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RIOT-OS%2Friotctrl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RIOT-OS%2Friotctrl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RIOT-OS%2Friotctrl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RIOT-OS%2Friotctrl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RIOT-OS","download_url":"https://codeload.github.com/RIOT-OS/riotctrl/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248602311,"owners_count":21131615,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["riot","riot-os","testing-framework","testing-tools"],"created_at":"2024-11-15T06:16:00.664Z","updated_at":"2025-04-12T17:06:30.730Z","avatar_url":"https://github.com/RIOT-OS.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"RIOT Ctrl\n=========\n\nThis provides python object abstraction of a RIOT device.\nThe first goal is to be the starting point for the serial abstraction and\nbuild on top of that to provide higher level abstraction like over the shell.\n\nIt could provide an RPC interface to a device in Python over the serial port\nand maybe also over network.\n\nThe goal is here to be test environment agnostic and be usable in any test\nframework and also without it.\n\n\nTesting\n-------\n\nRun `tox` to run the whole test suite:\n\n::\n\n    tox\n    ...\n    ________________________________ summary ________________________________\n      test: commands succeeded\n      lint: commands succeeded\n      flake8: commands succeeded\n      congratulations :)\n\nUsage\n-----\n\nRIOTCtrl provides a python object abstraction of a RIOT device. It’s\nmeant as a starting point for any serial abstraction on which higher\nlevel abstractions (like a shell) can be built.\n\n.. code:: python\n\n    from riotctrl.ctrl import RIOTCtrl\n\n    env = {'BOARD': 'native'}\n    # if not running from the application directory the a path must be provided\n    ctrl = RIOTCtrl(env=env, application_directory='.')\n    # flash the application\n    ctrl.make_run(['flash'])\n    # run the terminal through a contextmanager\n    with ctrl.run_term():\n        ctrl.term.expect('\u003e')       # wait for shell to start\n        ctrl.term.sendline(\"help\")  # send the help command\n        ctrl.term.expect('\u003e')       # wait for the command result to finnish\n        print(ctrl.term.before)     # print the command result\n    # run without a contextmanager\n    ctrl.start_term()               # start a serial terminal\n    ctrl.term.sendline(\"help\")      # send the help command\n    ctrl.term.expect('\u003e')           # wait for the command result to finnish\n    print(ctrl.term.before)         # print the command result\n    ctrl.stop_term()                # close the terminal\n\nCreating a RIOTCtrl object is done via environments. If empty then all\nconfiguration will come from the target application makefile. But any\nMake environment variable can be overridden, for example setting\n``BOARD`` to a target ``BOARD`` which is not the default for that\napplication.\n\nAny make target used on RIOT devices can be used on the abstraction\nlike: ``make flash`` =\u003e ``ctrl.make_run(['flash'])``.\n\n``ctrl.start_term()`` (``make term``\\ ’s alter ego) by default spawns a\n`pexpect \u003chttps://pexpect.readthedocs.io/en/stable/overview.html\u003e`__\nchild application. From there interactions with the application\nunder use can be atomized. In the example below the output of the\n``\"help\"`` command is captured:\n\nShellInteractions\n~~~~~~~~~~~~~~~~~\n\nRIOTCtrl provides a minimal extensions by using:\n`pexpect replwrap \u003chttps://pexpect.readthedocs.io/en/stable/api/replwrap.html\u003e`__\n“[A] Generic wrapper for read-eval-print-loops, a.k.a. interactive shells”.\nThis implements a nice wrapper for RIOT shell commands since it will wait for a\ncommand to finish before returning its output.\n\nRIOT already provides a ``ShellInteraction`` for the ``\"help\"`` command as well\nas many others. To make importing them as ``from riotctrl_shell.sys import Help``\npossible RIOT's `pythonlibs \u003chttps://github.com/RIOT-OS/RIOT/tree/master/dist/pythonlibs\u003e`__\nneeds to be part of the ``PYTHONPATH``, this can be done by setting in the environment\n``PYTHONPATH=$PYTHONPATH:${RIOTBASE}/dist/pythonlibs`` or doing so in the\nscript ``sys.path.append('/path/to/RIOTBASE/dist/pythonlibs')``\n\nThe previous example can be re-written using ``ShellInteraction``:\n\n.. code:: python\n\n    from riotctrl.ctrl import RIOTCtrl\n    from riotctrl.shell import ShellInteraction\n\n    env = {'BOARD': 'native'}\n    # if not running from the application directory the a path must be provided\n    ctrl = RIOTCtrl(env=env, application_directory='.')\n    # flash the application\n    ctrl.flash()                     # alias for ctrl.make_run(['flash'])\n    # shell interaction instance\n    shell = ShellInteraction(ctrl)\n    shell.start_term()               # start a serial terminal\n    print(shell.cmd(\"help\"))         # print the command result\n    shell.stop_term()                # close the terminal\n\nor using the already provided `Help \u003chttps://github.com/RIOT-OS/RIOT/blob/master/dist/pythonlibs/riotctrl_shell/sys.py#L16-L21\u003e`__\n``ShellInteraction``:\n\n.. code:: python\n\n    from riotctrl.ctrl import RIOTCtrl\n    from riotctrl_shell.sys import Help\n\n    env = {'BOARD': 'native'}\n    # if not running from the application directory the a path must be provided\n    ctrl = RIOTCtrl(env=env, application_directory='.')\n    # flash the application\n    ctrl.flash()                     # alias for ctrl.make_run(['flash'])\n    # shell interaction instance, Help uses the @ShellInteraction.check_term\n    # decorator, it will start the terminal if its not yet running, and close\n    # it after the command ends\n    shell = Help(ctrl)              # create ShellInteraction\n    print(shell.help())             # print the command result\n\nWriting ShellInteraction\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nLets use this simple C shell application as an example:\n\n.. code:: c\n\n    #include \u003cstdio.h\u003e\n    #include \u003cstdlib.h\u003e\n    #include \"shell.h\"\n\n    static unsigned int counter = 0;\n\n    static int _cmd_counter(int argc, char **argv)\n    {\n        if (argc == 1) {\n            printf(\"counter: %d\\n\", counter);\n        }\n        else if (argc == 2) {\n            counter += atoi(argv[1]);\n        }\n        else {\n            puts(\"Usage: counter [value]\");\n            return -1;\n        }\n        return 0;\n    }\n\n    static const shell_command_t shell_commands[] = {\n        { \"counter\", \"prints current counter or adds input\", _cmd_counter },\n        { NULL, NULL, NULL }\n    };\n\n    int main(void)\n    {\n        char line_buf[SHELL_DEFAULT_BUFSIZE];\n\n        shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE);\n\n        return 0;\n    }\n\nThis simple command allows to return the current counter value or modifying\nby adding a value to it.\n\n::\n\n    main(): This is RIOT! (Version: 2021.10-devel-645-g2c3266-pr_kconfig_mtd)\n    \u003e boardinfo\n    board: native\n    cpu: native\n    \u003e counter 5\n    \u003e counter -3\n    \u003e counter\n    counter: 2\n\nA ``ShellInteraction`` for this could look as follows:\n\n.. code:: python\n\n    from riotctrl.shell import ShellInteraction\n\n\n    class CounterCmdShell(ShellInteraction):\n        @ShellInteraction.check_term\n        def counter_cmd(self, args=None, timeout=-1, async_=False):\n            cmd = \"counter\"\n            if args is not None:\n                cmd += \" {args}\".format(args=\" \".join(str(a) for a in args))\n            return self.cmd(cmd, timeout=timeout, async_=False)\n\nParsing Interaction Results\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nParsers can be written for the result of ShellInteraction commands,\nthese can then be returned in any format, for this a base class\nShellInteractionParser is provided where the ``parse()`` method needs to\nbe implemented.\n\nAn examples for the ``counter`` command\n\n.. code:: python\n\n    import re\n    from riotctrl.shell import ShellInteractionParser\n\n\n    class CounterCmdShellParser(ShellInteractionParser):\n        pattern = re.compile(r\"counter: (?P\u003ccounter\u003e\\d+)$\")\n\n        def parse(self, cmd_output):\n            devices = None\n            for line in cmd_output.splitlines():\n                m = self.pattern.search(line)\n                if m is not None:\n                    return m.group[\"counter\"]\n\n.. code:: python\n\n    env = {'BOARD': 'native'}\n    # if not running from the application directory the a path must be provided\n    ctrl = RIOTCtrl(env=env, application_directory='.')\n    # flash the application\n    ctrl.flash()                     # alias for ctrl.make_run(['flash'])\n    # shell interaction instance\n    shell = CounterCmdShell(ctrl)\n     with ctrl.run_term():\n        parser = CounterCmdShellParser()\n        counter = parse.parse(shell_counter_cmd())\n        shell.counter_cmd(4)\n        assert counter + 4 = parse.parse(shell_counter_cmd())\n\nInteracting with multiple RIOT devices\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nRIOTCtrl only wrap’s a single RIOT device, handling multiple devices is\nnot yet handled in RIOTCtrl, but through different environments multiple\nRIOT devices can be created and controlled.\n\nUsers of RIOT and `FIT IoT-LAB \u003chttps://www.iot-lab.info/\u003e`__ may have\nalready ran experiments on multiple ctrls of the same type (e.g:\n``iotlab-m3``) using the ``IOTLAB_NODE`` make environment variable. With\nthis one can easily control which device it is targeting.\n\nBut if running this locally, with e.g.: multiple ``samr21-xpro``\nconnected the serial or ``DEBUG_ADAPTER_ID`` must be used to flash the\ncorrect device, and for some ``BOARD``\\ s also the serial port ``PORT``.\nThese variables can be appended to the environment of the spawned\nobject, e.g:\n\n-  `FIT IoT-LAB \u003chttps://www.iot-lab.info/\u003e`__:\n\n.. code:: python\n\n    # first device using dwm1001-1 on the saclay site\n    env1 = {'BOARD': 'dwm10001', 'IOTLAB_NODE': 'dwm1001-1.saclay.iot-lab.info'}\n    ctrl1 = RIOTCtrl(env=env1, application_directory='.')\n    # second device using dwm1001-2 on the saclay site\n    env2 = {'BOARD': 'dwm10001', 'IOTLAB_NODE': 'dwm1001-2.saclay.iot-lab.info'}\n    ctrl2 = RIOTCtrl(env=env2, application_directory='.')\n\n-  locally:\n\n.. code:: python\n\n    # first samr21-xpro\n    env1 = {'BOARD': 'samr21-xpro', 'DEBUG_ADAPTER_ID': 'ATML2127031800004957'}\n    ctrl1 = RIOTCtrl(env=env1, application_directory='.')\n    # second samr21-xpro\n    env2 = {'BOARD': 'samr21-xpro', 'DEBUG_ADAPTER_ID': 'ATML2127031800011458'}\n    ctrl2 = RIOTCtrl(env=env2, application_directory='.')\n\nFor the advanced user one could also do as suggested in\n`multiple-boards-udev \u003chttps://api.riot-os.org/advanced-build-system-tricks.html#multiple-boards-udev\u003e`__\nand use an easy to remember variable to identify BOARDs (which would\nallow also running the same python code on different setups), if\nfollowing the above guide:\n\n.. code:: python\n\n    # first samr21-xpro\n    env1 = {'BOARD': 'samr21-xpro', 'BOARD_NUM': 0}\n    ctrl1 = RIOTCtrl(env=env1, application_directory='.')\n    # second samr21-xpro\n    env2 = {'BOARD': 'samr21-xpro', 'BOARD_NUM': 1}\n    ctrl2 = RIOTCtrl(env=env2, application_directory='.')\n\nFactories\n~~~~~~~~~\n\nThe same tasks are done multiple times creating the object flashing it,\nstarting the terminal and making sure its clean up. Once experiments\ngrow and take over multiple ctrls this can become tedious, using a\nFactory together with a context manager can help with this.\n\nGoing back to our example lets write a factory inheriting from\n``RIOTCtrlBoardFactoryBase`` (or directly from ``RIOTCtrlFactoryBase``\nbase class).\n\n.. code:: python\n\n    from contextlib import ContextDecorator\n    from riotctrl.ctrl import RIOTCtrl, RIOTCtrlBoardFactory\n    from riotctrl_ctrl import native\n\n    class RIOTCtrlAppFactory(RIOTCtrlBoardFactory, ContextDecorator):\n\n        def __init__(self):\n            super().__init__(board_cls={\n                'native': native.NativeRIOTCtrl,\n            })\n            self.ctrl_list = list()\n\n        def __enter__(self):\n            return self\n\n        def __exit__(self, *exc):\n            for ctrl in self.ctrl_list:\n                ctrl.stop_term()\n\n        def get_ctrl(self, application_directory='.', env=None):\n            # retrieve a RIOTCtrl Object\n            ctrl = super().get_ctrl(\n                env=env,\n                application_directory=application_directory\n            )\n            # append ctrl to list\n            self.ctrl_list.append(ctrl)\n            # flash and start terminal\n            ctrl.flash()\n            ctrl.start_term()\n            # return ctrl with started terminal\n            return ctrl\n\nAnd the script itself can be re-written as:\n\n.. code:: python\n\n    with RIOTCtrlAppFactory() as factory:\n        env = {'BOARD': 'native'}\n        ctrl = factory.get_ctrl(env=env)\n        shell = SaulShell(ctrl)\n        parser = SaulShellCmdParser()\n        print(parser.parse(shell.saul_cmd()))\n\nGNRC Networking example native\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nLets put all the above into practice and script an experiment verifying\nconnectivity between two ctrls, here multiple ``native`` instance will\nbe used.\n\nFirst create two tap interfaces connected through a bridge interface,\ne.g. on linux:\n\n.. code:: shell\n\n    ip link add name tapbr0 type bridge\n    ip link set tapbr0 up\n    ip tuntap add dev tap0 mode tap user $USER\n    ip tuntap add dev tap1 mode tap user $USER\n    ip link set dev tap0 master tapbr0\n    ip link set dev tap1 master tapbr0\n    ip link set dev tap0 up\n    ip link set dev tap1 up\n\nThen we can ping and parse the results asserting than packet loss is\nunder a threshold or that an mount of responses was received..\n\n.. code:: python\n\n    from riotctrl_shell.gnrc import GNRCICMPv6Echo, GNRCICMPv6EchoParser\n    from riotctrl_shell.netif import Ifconfig\n\n\n    class Shell(Ifconfig, GNRCICMPv6Echo):\n      pass\n\n\n    with RIOTCtrlAppFactory() as factory:\n        # Create two native instances, specifying the tap interface\n        native_0 = factory.get_ctrl(env={'BOARD':'native', 'PORT':'tap0'})\n        native_1 = factory.get_ctrl(env={'BOARD':'native', 'PORT':'tap1'})\n        # `NativeRIOTCtrl` allows for `make reset` with `native`\n        native_0.reset()\n        native_1.reset()\n        # Perform a multicast ping and parse results\n        pinger = Shell(native_0)\n        parser = GNRCICMPv6EchoParser()\n        result = parser.parse(pinger.ping6(\"ff02::1\"))\n        # assert packetloss is under 10%\"))\n        assert result['stats']['packet_loss'] \u003c 10\n        # assert at least one responder\n        assert result['stats']['rx'] \u003e 0\n\nA more complex example can be seen in the Release Tests:\n`04-single-hop-6lowpan-icmp \u003chttps://github.com/RIOT-OS/Release-Specs/blob/master/04-single-hop-6lowpan-icmp/test_spec04.py\u003e`__\n\nExamples\n~~~~~~~~\n\n-  pytest: `ReleaseSpecs \u003chttps://github.com/RIOT-OS/Release-Specs\u003e`__\n-  unittests:\n    `tests/turo \u003chttps://github.com/RIOT-OS/RIOT/blob/master/tests/turo/tests/01-run.py\u003e`__,\n    `tests/congure_test \u003chttps://github.com/RIOT-OS/RIOT/blob/master/tests/congure_test/tests/01-run.py\u003e`__\n\nDiscussion\n~~~~~~~~~~\n\nRIOTCtrl base class is not tied into having a serial based interaction, its\nthe most common usage so far but a new interface or ``Interaction`` could\nuse different different transports (e.g. COAP), and does not need to provide\na CLI type interface.\n\nTest applications could also use Structured Output, like RIOT's\n`turo \u003chttps://doc.riot-os.org/group__test__utils__result__output.html\u003e`__,\nand in this case parsing CBOR/JSON/XML output could be close to a NOP.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Friot-os%2Friotctrl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Friot-os%2Friotctrl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Friot-os%2Friotctrl/lists"}