diff --git a/docs/how-tos/create-module.md b/docs/how-tos/create-module.md index da7ae33e69..6182aabfcf 100644 --- a/docs/how-tos/create-module.md +++ b/docs/how-tos/create-module.md @@ -163,9 +163,9 @@ In this step, you will code the logic that is unique to your model. {{% alert title="Tip (optional)" color="tip" %}} -If you are using Golang, use the [Golang Module templates](https://github.com/viam-labs/module-templates-golang) which contain detailed instructions for creating your module. +If you are using Python or Golang, use the [`viam module generate` CLI command](/cli/#module) to generate the scaffolding for a module with one resource model. -If you are using Python, you can use the [Viam module generator](https://github.com/viam-labs/generator-viam-module/) to generate the scaffolding for a module with one resource model. +For a step-by-step guide that uses the CLI module generator, see [Create a Hello World module](/how-tos/hello-world-module/). {{% /alert %}} diff --git a/docs/how-tos/hello-world-module.md b/docs/how-tos/hello-world-module.md index 8bc6242c54..7e386dc562 100644 --- a/docs/how-tos/hello-world-module.md +++ b/docs/how-tos/hello-world-module.md @@ -764,7 +764,7 @@ To package (for Python) and upload your module and make it available to configur {{< tabs >}} {{% tab name="Python" %}} -1. Package the module as an archive, run the following command from inside the hello-world directory: +1. To package the module as an archive, run the following command from inside the hello-world directory: ```sh {id="terminal-prompt" class="command-line" data-prompt="$"} tar -czf module.tar.gz run.sh setup.sh requirements.txt src diff --git a/docs/how-tos/sensor-module.md b/docs/how-tos/sensor-module.md index ae3467424f..a65f4e2c55 100644 --- a/docs/how-tos/sensor-module.md +++ b/docs/how-tos/sensor-module.md @@ -29,12 +29,29 @@ Making a module to support your sensor will allow you to use it with Viam's data 1. [Start with a test script](#start-with-a-test-script) 1. [Generate template module code](#generate-template-module-code) 1. [Implement the sensor API](#implement-the-sensor-api) -1. [Make the module executable](#make-the-module-executable) 1. [Test your module locally](#test-your-module-locally) 1. [Upload your module](#upload-your-module-to-the-registry) {{% /alert %}} +## Prerequisites + +{{< expand "Install the Viam CLI and authenticate" >}} +Install the Viam CLI and authenticate to Viam, from the same machine that you intend to upload your module from. + +{{< readfile "/static/include/how-to/install-cli.md" >}} + +Authenticate your CLI session with Viam using one of the following options: + +{{< readfile "/static/include/how-to/auth-cli.md" >}} +{{< /expand >}} + +{{% expand "Install viam-server on your computer and connect to the Viam app" %}} + +{{% snippet "setup.md" %}} + +{{% /expand%}} + ## Start with a test script Start by getting a test script working so you can check that your sensor code itself works before packaging it into a module. @@ -143,71 +160,28 @@ Run your test script from your terminal and make sure you are able to get readin There are a few standardized files that must be part of any module. You can create these automatically using the Viam module generator: -{{< table >}} -{{% tablestep %}} -**1. Install the module generator** - -Install the [module generator](https://github.com/viam-labs/generator-viam-module/): - -1. Install node (version 16 or later) and npm if you don't already have them: - - ```sh {id="terminal-prompt" class="command-line" data-prompt="$"} - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash - nvm install 16 - nvm use 16 - ``` - -1. Install Yeoman: +1. Run the `module generate` command in your terminal: ```sh {id="terminal-prompt" class="command-line" data-prompt="$"} - npm install -g yo + viam module generate ``` -1. Now install the module generator: - - ```sh {id="terminal-prompt" class="command-line" data-prompt="$"} - npm install -g generator-viam-module - ``` - -{{% /tablestep %}} -{{% tablestep %}} -**2. Run the generator** - -Go to the directory where you want to create your module and run the generator: - -```sh {id="terminal-prompt" class="command-line" data-prompt="$"} -yo viam-module -``` - -{{% /tablestep %}} -{{% tablestep %}} -**3. Name your model** - -When prompted for a model triplet, use `::`. -For example, `jessamy:weather:meteo_PM`. - -- You can find your organization namespace by going to your organization settings in the [Viam app](https://app.viam.com). -- The repo name (also called family name) is generally the name of the GitHub repo where you will put your module code. - Name it something related to what your module does. -- Name your sensor based on what it supports, for example, if it supports a model of ultrasonic sensor called "XYZ Sensor 1234" you could call your model `XYZ_1234` or similar. - -For more information, see [Name your new resource model](/how-tos/create-module/#name-your-new-resource-model). - -{{% /tablestep %}} -{{% tablestep %}} -**4. Specify the API** - -For the API triplet, enter `rdk:component:sensor`. -This means that you are implementing the standard Viam sensor API. - -When asked whether this is a Viam SDK built-in API, enter `yes`. +2. Follow the prompts, naming your module and selecting from the options. -{{% /tablestep %}} -{{< /table >}} -
+ +| Prompt | Description | +| -------| ----------- | +| Module name | The module name describes the module or the family of devices it supports. It is generally the same as the name of the GitHub repo where you will put your module code. For example, `weather`. | +| Language | The language for the module. To follow this guide, choose Python. | +| Visibility | Choose `Private` to share only with your organization, or `Public` to share publicly with all organizations. If you are testing, choose `Private`. | +| Namespace/Organization ID | In the [Viam app](https://app.viam.com), navigate to your organization settings through the menu in upper right corner of the page. Find the **Public namespace** and copy that string. In the example snippets below, the namespace is `jessamy`. | +| Resource to add to the module (API) | The [component API](/appendix/apis/#component-apis) or [service API](/appendix/apis/#service-apis) the resource you're creating implements. Choose `Sensor Component` for this guide. | +| Model name | Name your sensor based on what it supports, for example, if it supports a model of ultrasonic sensor called “XYZ Sensor 1234” you could call your model `XYZ_1234` or similar. | +| Enable cloud build | You can select `No` for this guide because you'll build the module yourself before uploading it. If you select `Yes` and push the generated files (including the .github folder) and create a release of the format `vX.X.X`, the module will build and upload to the Viam registry. | +| Register module | Select `Yes` unless you are creating a local-only module for testing purposes and do not intend to upload it. | -The generator creates a `run.sh` file, a `requirements.txt` file, a readme, and source code files. -In the next section, you'll customize some of these files to support your sensor. +The generator will create a folder containing stub files for your modular sensor component. +In the next section, you'll customize some of the generated files to support your sensor. ## Implement the sensor API @@ -218,18 +192,19 @@ You need to implement this method so your sensor supports the sensor API: {{% tablestep %}} **1. Edit configuration code** -In the generated /YOUR_MODULE_NAME/src/ directory, open the MODEL_NAME.py file. +In the generated /YOUR_MODULE_NAME/src/ directory, open the main.py file. Edit the config attributes to fit your sensor. -For example, if your sensor requires two pins, copy the `some_pin` lines and add another pin with a different name. -If you want to be able to configure something else, for example the location to get online data from, you can add attributes for that. -If your sensor doesn't require any configuration, delete the `some_pin` lines but don't delete the `validate` and `reconfigure` functions entirely; they're needed for the module to function even if they don't actually validate the input or reconfigure the resource. +For example, if your sensor requires two pins, edit the validate function to check that they are configured. +Edit the reconfigure function to get the configured values of each parameter from the configuration. +If you want to be able to configure something else, for example the location to get online data from, you can add attributes for that (see example code in the expander below). +If your sensor doesn't require any configuration, leave the `validate` and `reconfigure` functions as they are; they're needed for the module to function even if they don't actually validate the input or reconfigure the resource. {{% /tablestep %}} {{% tablestep %}} **2. Define `get_readings`** -In the `get_readings` function definition, paste your test script. +In the `get_readings` function definition, replace `raise NotImplementedError()` by pasting your test script. Edit the script to return a dictionary of readings instead of printing them. Be sure to add any required imports to the top of the file. @@ -248,52 +223,45 @@ Be sure to add any required imports to the top of the file. The following code puts the functionality of the [example test script](#start-with-a-test-script) into the `get_readings` function definition. ```python {class="line-numbers linkable-line-numbers"} -# meteo_PM.py -from typing import ClassVar, Mapping, Any, Optional -from typing_extensions import Self +import asyncio +from typing import Any, ClassVar, Final, Mapping, Optional, Sequence -from viam.utils import SensorReading, struct_to_dict -from viam.module.types import Reconfigurable +from typing_extensions import Self +from viam.components.sensor import Sensor +from viam.module.module import Module from viam.proto.app.robot import ComponentConfig from viam.proto.common import ResourceName from viam.resource.base import ResourceBase +from viam.resource.easy_resource import EasyResource from viam.resource.types import Model, ModelFamily -from viam.components.sensor import Sensor -from viam.logging import getLogger +from viam.utils import SensorReading, struct_to_dict import openmeteo_requests import requests_cache from retry_requests import retry -LOGGER = getLogger(__name__) - - -class meteo_PM(Sensor, Reconfigurable): - - """ - Sensor represents a sensing device that can provide measurement readings. - """ +class Meteopm(Sensor, EasyResource): MODEL: ClassVar[Model] = Model( ModelFamily("jessamy", "weather"), "meteo_PM") - # Class parameters - latitude: float # Latitude at which to get data - longitude: float # Longitude at which to get data - - # Constructor @classmethod def new( - cls, config: ComponentConfig, - dependencies: Mapping[ResourceName, ResourceBase] - ) -> Self: - my_class = cls(config.name) - my_class.reconfigure(config, dependencies) - return my_class - - # Validates JSON Configuration + cls, config: ComponentConfig, dependencies: Mapping[ + ResourceName, ResourceBase] + ) -> Self: + """This method creates a new instance of this Sensor component. + The default implementation sets the name from the `config` parameter + and then calls `reconfigure`. + """ + return super().new(config, dependencies) + @classmethod - def validate(cls, config: ComponentConfig): + def validate_config(cls, config: ComponentConfig) -> Sequence[str]: + """This method allows you to validate the configuration object + received from the machine, as well as to return any implicit + dependencies based on that `config`. + """ fields = config.attributes.fields # Check that configured fields are floats if "latitude" in fields: @@ -303,28 +271,31 @@ class meteo_PM(Sensor, Reconfigurable): if "longitude" in fields: if not fields["longitude"].HasField("number_value"): raise Exception("Longitude must be a float.") - return + return [] - # Handles attribute reconfiguration def reconfigure( - self, config: ComponentConfig, - dependencies: Mapping[ResourceName, ResourceBase] - ): + self, config: ComponentConfig, dependencies: Mapping[ + ResourceName, ResourceBase] + ): + """This method allows you to dynamically update your service + when it receives a new `config` object. + """ attrs = struct_to_dict(config.attributes) self.latitude = float(attrs.get("latitude", 45)) - LOGGER.debug("Using latitude: " + str(self.latitude)) + self.logger.debug("Using latitude: " + str(self.latitude)) self.longitude = float(attrs.get("longitude", -121)) - LOGGER.debug("Using longitude: " + str(self.longitude)) - - return + self.logger.debug("Using longitude: " + str(self.longitude)) + return super().reconfigure(config, dependencies) async def get_readings( - self, *, extra: Optional[Mapping[str, Any]] = None, - timeout: Optional[float] = None, **kwargs + self, + *, + extra: Optional[Mapping[str, Any]] = None, + timeout: Optional[float] = None, + **kwargs ) -> Mapping[str, SensorReading]: - # Set up the Open-Meteo API client with cache and retry on error cache_session = requests_cache.CachedSession( '.cache', expire_after=3600) @@ -351,13 +322,17 @@ class meteo_PM(Sensor, Reconfigurable): current_pm10 = current.Variables(0).Value() current_pm2_5 = current.Variables(1).Value() - LOGGER.info(current_pm2_5) + self.logger.info(current_pm2_5) # Return a dictionary of the readings return { "pm2_5": current_pm2_5, "pm10": current_pm10 } + + +if __name__ == "__main__": + asyncio.run(Module.run_from_registry()) ``` {{< /expand >}} @@ -370,15 +345,6 @@ Most modules have their implementation code linked on their module page, so you Update the generated requirements.txt file to include any packages that must be installed for the module to run. Depending on your use case, you may not need to add anything here beyond `viam-sdk` which is auto-populated. -## Make the module executable - -You need an executable file so that `viam-server` can run your module. -The module generator already created the run.sh "entrypoint" file for you, so all you need to do is make this file executable by running the following command with your correct file path: - -```sh {id="terminal-prompt" class="command-line" data-prompt="$"} -sudo chmod +x /run.sh -``` - ## Test your module locally {{% expand "Prerequisite: A running machine connected to the Viam app." %}} @@ -394,7 +360,20 @@ It's a good idea to test your module locally before uploading it to the [Viam Re {{< table >}} {{% tablestep link="/how-tos/create-module/#test-your-module-locally" %}} -**1. Configure your local module on a machine** +**1. Set up a virtual environment** + +Create a virtual Python environment with the necessary packages by running the setup file from within the hello-world directory: + +```sh {id="terminal-prompt" class="command-line" data-prompt="$"} +sh setup.sh +``` + +This environment is where the local module will run. +`viam-server` does not need to run inside this environment. + +{{% /tablestep %}} +{{% tablestep %}} +**2. Configure your local module on a machine** On your machine's **CONFIGURE** tab in the [Viam app](https://app.viam.com), click the **+** (create) icon in the left-hand menu. Select **Local module**, then **Local module**. @@ -403,7 +382,7 @@ Type in the _absolute_ path on your machine's filesystem to your module's execut Click **Create**. {{% /tablestep %}} -{{% tablestep link="/how-tos/create-module/#test-your-module-locally" %}} +{{% tablestep %}} **2. Configure the model provided by your module** Click the **+** button again, this time selecting **Local module** and then **Local component**.