Let's say you have a machine in Viam with a sensor and an actuator, like a webcam and a lamp plugged into a smart outlet. You want the machine to monitor the video feed, in this case, turning on the lamp when a person is detected.

And while you could run this logic from a laptop, a cloud server, or a web app, running it on the device itself ensures it works reliably and in real time, even without internet connectivity or external dependencies.

To do this, you'll write a control module that checks sensor data and triggers actions automatically. For example, in the automatic plant watering workshop, the control module is the brains behind the real-world robot that orchestrates activities between sensors and actuators.

control logic workflow for plant watering

In this codelab, you'll learn how to build and deploy control logic in a module running directly on the machine, for this or any other use case.

control logic workflow

What You'll Build

Prerequisites

What You'll Need

What You'll Learn

Watch the Video

Follow along in this how-to video.

Configure your machine

  1. In the Viam app under the LOCATIONS tab, create a machine by typing in a name and clicking Add machine. add machine
  2. Click View setup instructions. setup instructions
  3. To install viam-server on the Raspberry Pi device that you want to use to communicate with and control your webcam, select the Linux / Aarch64 platform for the Raspberry Pi, and leave your installation method as viam-agent. select platform
  4. From the terminal window, run the following command to SSH (Secure Shell) into your board, where the text in <> should be replaced (including the < and > symbols themselves) with the user and hostname you configured when you set up your machine.
    ssh <USERNAME>@<REMOTE-HOSTNAME>.local
    
  5. Use the viam-agent to download and install viam-server on your Raspberry Pi. Follow the instructions to run the command provided in the setup instructions from the SSH prompt of your Raspberry Pi. installation agent
  6. The setup page will indicate when the machine is successfully connected. successful toast

Configure your peripherals

  1. Optional: Now that you set up a machine, configure any hardware and software resources that you will use with your machine and that you want to drive with your control logic. For example, you could use this same pattern to turn on a fan when a temperature sensor crosses a threshold, or trigger a motor when an object is detected.Or you can skip this step and proceed to the next section.

In this section, let's learn how to create a module and deploy the control logic to our machine.

Generate stub files

  1. On your working computer, open your terminal window, and make sure you are still logged in to the Viam CLI
    viam login
    
  2. Run the command to generate a Viam module.
    viam module generate
    
  3. Follow the prompts, selecting the following options:
    • Module name: control-lamp-alarm
      • This can be anything you choose to identify your control logic module.
    • Language: Python
    • Visibility: Private
    • Namespace/Organization ID: In the Viam app, navigate to your organization settings through the menu in upper right corner of the page. Find the Public namespace and copy that string. public namespace
    • Resource to be added to the module: Generic Service
      • This is a Generic Service. If you choose a different resource type to add your control logic to, you must implement any required API methods for the chosen component.
    • Model name: lamp-alarm
      • This can be anything you choose to identify your control logic model.
    • Enable cloud build: Yes
      • This enables Viam to build your module via GitHub action.
    • Register module: Yes
      • This lets Viam know that the module exists.
  4. Press the Enter key and the generator will create a folder for your control logic component.gif of running viam module generator and prompt selections
  5. Open the code files using your preferred IDE. Find the Python file within the src/models/ directory with the same name as your model name, lamp_alarm.py shown here. This is where we can add our control logic. generated code files

Next, let's add our control logic.

Now that you created the initial module, let's add our control logic, and deploy it to our Raspberry Pi.

Final code sample: You can refer to the sample code to see how everything fits together. In this section, let's break it down step-by-step to learn how the module works.

Update the module code

generated code files

  1. Review scaffolding: Let's take a closer look at the generated files, and find the Python file within the src/models/ directory with the same name as your model name, lamp_alarm.py shown here.
    • Review the other generated files within the project directory
    • Review the imports within lamp_alarm.py
    • Review the class definition within lamp_alarm.py
      • It includes the public namespace, module name, and model name that you specified in the module generation prompts (joyce, control-lamp-alarm, and lamp-alarm shown here).
      • It also includes four default class methods new(), validate_config(), reconfigure(), and do_command().
  2. Add dependencies: Add these imports near the top of the file to support our control loop and provide more visibility during debugging. If your control logic requires external dependencies, be sure to also update the requirements.txt file.
    import asyncio
    from threading import Event
    from viam.logging import getLogger
    
  3. Initialize helper elements: Initialize the Viam logger after the imports so that we have more visibility during development. The name of the logger is the model name, lamp-alarm shown here.
    LOGGER = getLogger("lamp-alarm")
    
  4. Initialize variables in the class: At the beginning of the class definition, initialize some variables to support the control loop.
    running = None
    task = None
    event = Event()
    
  5. Add helper functions: Within the class definition, add some functions to support the control loop.
    def start(self):
           loop = asyncio.get_event_loop()
           self.task = loop.create_task(self.control_loop())
           self.event.clear()
    
     def stop(self):
         self.event.set()
         if self.task is not None:
             self.task.cancel()
    
     async def control_loop(self):
         while not self.event.is_set():
             await self.on_loop()
             await asyncio.sleep(0)
    
     async def on_loop(self):
         try:
             self.logger.info("Executing control logic")
             # TODO: ADD CONTROL LOGIC
    
         except Exception as err:
             self.logger.error(err)
         await asyncio.sleep(10)
    
     def __del__(self):
         self.stop()
    
     async def close(self):
         self.stop()
    
  6. Add control logic: Within your newly defined on_loop() method, you can add custom control logic. Swap in your own logic here! Replace the example vision and lamp control with your own sensor and actuator. If your goal is a working version that does nothing, proceed to the next step.
  7. Validate configuration: In the validate_config() method, you can ensure the user correctly provides the required dependencies, formatted as expected. Refer to the docs for more details about module dependencies. If your goal is a working version that validates nothing, proceed to the next step.
  8. Initialize required resources: In the reconfigure() method, initialize any required resources if your control logic relies on other parts of the machine (like sensors or services). This method is called when your model is first added to the machine, and again whenever the machine configuration is updated.
  9. Start the control loop: In the reconfigure() method, start the control logic to run in a background loop.
    def reconfigure(
      self, config: ComponentConfig,
      dependencies: Mapping[ResourceName, ResourceBase]
      ):
      # starts automatically
      if self.running is None:
          self.start()
      else:
          LOGGER.info("Already running control logic.")
    
  10. Add Do Command for manual testing: Replace the do_command() method with the following code, so that we can manually test the control loop with stop and start.
    async def do_command(
        self,
        command: Mapping[str, ValueTypes],
        *,
        timeout: Optional[float] = None,
        **kwargs
    ) -> Mapping[str, ValueTypes]:
    
        result = {key: False for key in command.keys()}
        for name, args in command.items():
            if name == "action" and args == "start":
                self.start()
                result[name] = True
            if name == "action" and args == "stop":
                self.stop()
                result[name] = True
        return result
    

Now that you've added control logic to the module, let's test it locally on our device.

Configure hot reloading

  1. Create reloading script: Since we enabled cloud build during the module generation, we can create a hot reloading script to bundle and run our code. Create a reloading script file called reload.sh in the project root directory.
    touch reload.sh
    
  2. Copy and paste the following code into the new file.
    #!/usr/bin/env bash
    
    # bash safe mode. look at `set --help` to see what these are doing
    set -euxo pipefail
    
    cd $(dirname $0)
    MODULE_DIR=$(dirname $0)
    VIRTUAL_ENV=$MODULE_DIR/venv
    PYTHON=$VIRTUAL_ENV/bin/python
    ./setup.sh
    
    # Be sure to use `exec` so that termination signals reach the python process,
    # or handle forwarding termination signals manually
    exec $PYTHON src/main.py $@
    
  3. Grant permissions: Make the reload script executable by running the following command.
    chmod 755 reload.sh
    
  4. Set up virtual environment: Create a virtual Python environment with the necessary packages by running the setup.sh file.
    sh setup.sh
    
  5. Point to reload script: In the meta.json file, replace the path for entrypoint, build, and path.
    "entrypoint": "reload.sh",
    "first_run": "",
    "build": {
      "build": "rm -f module.tar.gz && tar czf module.tar.gz requirements.txt src/*.py src/models/*.py meta.json setup.sh reload.sh",
      "setup": "./setup.sh",
      "path": "module.tar.gz",
      "arch": [
        "linux/amd64",
        "linux/arm64"
      ]
    }
    
  6. Get Part ID: To run the module code on a different machine besides the computer that you are working on, Raspberry Pi in this example, you'll need the part ID of the remote machine. Go to the machine status dropdown in the Viam app, and copy the part ID to your clipboard. part ID in machine status dropdown
  7. Add module to the device: From the command line, bundle and move the module code to the target machine with viam module reload and include the -part-id flag and replace the placeholder with your own part ID from the previous step. Doing this also registers and restarts the module.
    viam module reload --part-id ff05e799-e323-4027-84b8-5c703bcf652f
    
  8. Once the script completes, you can see two new elements in the Viam app under the CONFIGURE tab.
    • A shell service for Viam to connect to your target device.
    • Your local module called control-lamp-alarm. shell service and local module
  9. Click the + icon in the left-hand menu and select Local module and then Local service to create a new local service called controller.
    • Enter the model namespace triplet, for example joyce:control-lamp-alarm:lamp-alarm. You can find the triplet in the model field of your meta.json file.
    • Select Type: generic
    • Enter the module Name: controllerlocal module configuration
  10. Configure dependencies: Configure any required dependencies using proper JSON syntax.
  11. Save your changes in the top right and wait a few moments for the configuration changes to take effect.

Continue refining the control logic

As you continue debugging and making updates to your control logic, use viam module reload for Viam to rebuild the module and restart the instance on your machine with the latest code, again using your part ID.

viam module reload --part-id ff05e799-e323-4027-84b8-5c703bcf652f

Now that your module is working the way you want, upload it to the Viam registry so that it can be re-used by your future self, your team, or the general public.

Publish the module

  1. Update README: Update the README.md file, following the suggested template. This provides details to your future self and others about what your module does and how to use it.
  2. Add GitHub repository link: If you haven't already, set up the GitHub repository where you plan to commit the module source code. Add a link to that GitHub repository as the url in the meta.json file. This is required for the cloud build to work. GitHub URL in meta.json
  3. Update metadata: Make any final edits to the meta.json file such as including a description or updating the visibility to public if you want the module to be visible to all Viam users.
  4. Package and upload: Revert the edits you made in the meta.json for local testing, so the values for entrypoint, build, and path are in the original state as shown below.
    "entrypoint": "dist/main",
    "first_run": "",
    "build": {
      "build": "./build.sh",
      "setup": "./setup.sh",
      "path": "dist/archive.tar.gz",
      "arch": [
        "linux/amd64",
        "linux/arm64"
      ]
    }
    
  5. Update module: From the CLI, run viam module update, which is required when you make changes to the meta.json.
    viam module update
    
  6. Commit and push your updates to GitHub: Commit and push your latest updates to your remote GitHub repository.
  7. Set up GitHub action: The viam module generate command already generated the build-action file in your .github/workflows folder, so you just need to set up authentication in GitHub, and then create a new release to trigger the action. In your terminal, run the following command to view your organization ID.
    viam organizations list
    
  8. Create API key: Create an API key for your organization, using your organization ID and a name for the API key.
    viam organization api-key create --org-id YOUR_ORG_UUID --name descriptive-key-name
    
  9. Add new repository secrets: In the GitHub repository for your project, go to SettingsSecrets and variablesActions. Add repository secret
  10. Create new secrets: Create two new secrets using the New repository secret button:
    • VIAM_KEY_ID with the UUID from Key ID in your terminal
    • VIAM_KEY_VALUE with the string from Key Value in your terminal GitHub repository secrets
  11. Create a new release: From the main code page of your GitHub repo, find Releases in the right side menu and click Create a new release. GitHub create release
  12. Add a release tag: In the Choose a tag dropdown, create a new tag such as 1.0.0. Do not prepend the tag with v or the GitHub action will not trigger. For details about versioning, see Module versioning. GitHub choose tag
  13. Publish release: Click Publish release. The cloud build action will begin building the new module version for each architecture listed in your meta.json. GitHub publish release

Add your new modular resource to your machines

Now that your module is in the registry, you can configure the component you added on your machines just as you would configure other components and services.

  1. Delete local module configuration: In the Viam app under your machine's CONFIGURE tab, delete the resource. There's no more need for local module configuration since it is primarily for testing purposes. In our example, the local module configuration was called controller.
  2. Delete local module: For the same reasons, delete the local module. In our example, the local module was called control-lamp-alarm. delete local module
  3. Add modular resource: Click the + button, select "Component or service", and search for your model name to add your new modular resource. add modular resource
  4. Configure the modular resource with the required attributes. Notice the beautiful documentation that you impressively added to your module, and the label in the top right corner of the resource panel indicating the module comes from the Viam registry. new modular resource
  5. Save your changes in the top right and wait a few moments for the configuration changes to take effect.

What you learned

Advanced scenarios for Viam modules

At this point, you have created and tested your own control logic module. See other examples that implement control logic:

You can also create your own module to integrate custom functionality for other components and services.

More about the Viam Registry

Modules are a very important part of the Viam platform. They enable robots to be built in a composable and versatile manner. Browse more modules in the Viam registry.

Related Viam resources