diff --git a/docs/cli.md b/docs/cli.md index baec5784bb..933d3b6448 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -628,7 +628,7 @@ This includes: - Building your module for different architectures using cloud runners - Building a module locally and running it on a target device. Rebuilding & restarting if already running. -See [Upload a custom module](/how-tos/upload-module/) and [Update an existing module](/how-tos/manage-modules/#update-an-existing-module) for more information. +See [Upload a module](/how-tos/upload-module/) and [Update an existing module](/how-tos/manage-modules/#update-an-existing-module) for more information. If you update and release your module as part of a continuous integration (CI) workflow, you can also [automatically upload new versions of your module on release](/how-tos/manage-modules/#update-an-existing-module-using-a-github-action) using a GitHub Action. @@ -718,7 +718,7 @@ viam module upload --version=1.0.0 --platform=darwin/arm64 packaged-module.tar.g | `--org-id` | The organization ID to associate the module to. See [Using the `--org-id` argument](#using-the---org-id-and---public-namespace-arguments) | `create`, `upload` | **Required** | | `--public-namespace` | The [namespace](/cloud/organizations/#create-a-namespace-for-your-organization) to associate the module to. See [Using the `--public-namespace` argument](#using-the---org-id-and---public-namespace-arguments) | `create`, `upload` | **Required** | | `--platform` | The architecture of your module binary. See [Using the `--platform` argument](#using-the---platform-argument) | `upload`, `build logs` | **Required** | -| `--tags` | Comma-separated list of platform tags that determine to which this binary can be deployed. Examples: `distro:debian,distro:ubuntu, os_version:22.04,os_codename:jammy`. For a machine to use an uploaded binary, all tags must be satisfied as well as the `--platform` field. | `upload` | Optional | +| `--tags` | Comma-separated list of platform tags that determine to which platforms this binary can be deployed. Examples: `distro:debian,distro:ubuntu, os_version:22.04,os_codename:jammy`. For a machine to use an uploaded binary, all tags must be satisfied as well as the `--platform` field. | `upload` | Optional | | `--version` | The version of your module to set for this upload. See [Using the `--version` argument](#using-the---version-argument) | `upload` | **Required** | | `--wait` | Wait for the build to finish before outputting any logs | `build logs` | Optional | @@ -863,7 +863,7 @@ In the example above, the model namespace is set to `acme` to match the owning o If the two namespaces do not match, the command will return an error. {{% /alert %}} -See [Upload a custom module](/how-tos/upload-module/) and [Update an existing module](/how-tos/manage-modules/#update-an-existing-module) for a detailed walkthrough of the `viam module` commands. +See [Upload a module](/how-tos/upload-module/) and [Update an existing module](/how-tos/manage-modules/#update-an-existing-module) for a detailed walkthrough of the `viam module` commands. See [Modular resources](/registry/) for a conceptual overview of modules and the modular resource system at Viam. diff --git a/docs/how-tos/hello-world-module.md b/docs/how-tos/hello-world-module.md index 7ebcc44c08..8bc6242c54 100644 --- a/docs/how-tos/hello-world-module.md +++ b/docs/how-tos/hello-world-module.md @@ -79,27 +79,43 @@ print("Hello, World! The latest random number is ", number, ".") ``` {{% /tab %}} - - - +{{% /tab %}} {{< /tabs >}} 1. Replace the path in the script above with the path to where you saved your photo. @@ -120,16 +136,25 @@ pip install Pillow python3 test.py ``` +The image you saved should open on your screen, and a random number should print to your terminal. + +In later steps, the module generator will create a new virtual environment with required dependencies, so you can deactivate the one you just ran the test script in: + +```sh {id="terminal-prompt" class="command-line" data-prompt="$"} +deactivate +``` + {{% /tab %}} -{{< /tabs >}} +{{% tab name="Go" %}} - The image you saved should open on your screen, and a random number should print to your terminal. +```sh {id="terminal-prompt" class="command-line" data-prompt="$"} +go run test.go +``` -1. In later steps, the module generator will create a new virtual environment with required dependencies, so you can deactivate the one you just ran the test script in: +The image you saved should open on your screen, and a random number should print to your terminal. - ```sh {id="terminal-prompt" class="command-line" data-prompt="$"} - deactivate - ``` +{{% /tab %}} +{{< /tabs >}} ## Choose an API to implement @@ -180,7 +205,7 @@ First let's generate the camera component files, and we'll add the sensor code l We will add the sensor later. - Model name: `hello-camera` - Enable cloud build: `No` - - Register module: `No` + - Register module: `Yes` 1. Hit your Enter key and the generator will generate a folder called hello-world containing stub files for your modular camera component. @@ -210,10 +235,13 @@ You need to add some sensor-specific code to support the sensor component. - Enable cloud build: `No` - Register module: `No` -1. Open temporary/src/main.py. - Copy the sensor class definition, from `class HelloSensor(Sensor, EasyResource)` through the `get_readings()` function definition (lines 15-65). +{{< tabs >}} +{{% tab name="Python" %}} - Open the hello-world/src/main.py file you generated earlier, and paste the sensor class definition in after the camera class definition, above `if __name__ == "__main__":`. +3. Open temporary/src/main.py. + Copy the sensor class definition, from `class HelloSensor(Sensor, EasyResource)` through the `get_readings()` function definition (lines 15-65).

+ + Open the hello-world/src/main.py file you generated earlier, and paste the sensor class definition in after the camera class definition, above `if __name__ == "__main__":`.

1. Change `temporary` to `hello-world` in the ModelFamily line, so you have, for example: @@ -230,8 +258,82 @@ You need to add some sensor-specific code to support the sensor component. Save the hello-world/src/main.py file. -1. Open temporary/meta.json and copy the model information. - For example: +{{% /tab %}} +{{% tab name="Go" %}} + +3. Edit the file structure:

+ + 1. Change the name of hello-world/models/module.go to hello-camera.go.

+ + 1. Change the name of temporary/models/module.go to hello-sensor.go. + Move the hello-sensor.go folder from temporary/models/ to /hello-world/models/.

+ +1. Open hello-world/main.go. + You need to add the necessary imports and define how it adds the sensor model from the registry. + Delete all the contents and replace them with the following:

+ + ```go {class="line-numbers linkable-line-numbers" data-start="29"} + package main + + import ( + "context" + "hello-world/models" + + "go.viam.com/rdk/components/camera" + "go.viam.com/rdk/components/sensor" + "go.viam.com/rdk/logging" + "go.viam.com/rdk/module" + "go.viam.com/utils" + ) + + func main() { + utils.ContextualMain(mainWithArgs, module.NewLoggerFromArgs("hello-world")) + } + + func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) error { + helloWorld, err := module.NewModuleFromArgs(ctx) + if err != nil { + return err + } + + if err = helloWorld.AddModelFromRegistry(ctx, camera.API, models.HelloCamera); err != nil { + return err + } + + if err = helloWorld.AddModelFromRegistry(ctx, sensor.API, models.HelloSensor); err != nil { + return err + } + + err = helloWorld.Start(ctx) + defer helloWorld.Close(ctx) + if err != nil { + return err + } + + <-ctx.Done() + return nil + } + ``` + + Save the file.

+ +1. Change all instances of `temporary` in hello-world/models/hello-sensor.go:

+ + 1. Edit `temporary` to `hello-world` on line 14, so it looks like this (but with your org ID in place of `jessamy`):

+ + ```go {class="line-numbers linkable-line-numbers" data-start="14"} + HelloSensor = resource.NewModel("jessamy", "hello-world", "hello-sensor") + ``` + + 1. Change both instances of `newTemporaryHelloSensor` to `newHelloWorldHelloSensor`.

+ + 1. Search for any other instances of `temporary` in the rest of the hello-world/models/hello-sensor.go and replace each with `helloWorld`. + +{{% /tab %}} +{{< /tabs >}} + +6. Open temporary/meta.json and copy the model information. + For example:

```json {class="line-numbers linkable-line-numbers" data-start="8"} { @@ -240,11 +342,11 @@ You need to add some sensor-specific code to support the sensor component. } ``` -1. Open hello-world/meta.json and paste the sensor model into the model list. +1. Open hello-world/meta.json and paste the sensor model into the model list.

- Edit the `description` to accurately include both models. + Edit the `description` to include both models.

- Change `temporary` to `hello-world`. + Change `temporary` to `hello-world`.

The file should now resemble the following: @@ -390,6 +492,169 @@ You don't need to edit any of the validate or configuration methods because you' {{< /expand >}} +{{% /tab %}} +{{% tab name="Go" %}} + +### Implement the camera API + +First, implement the camera API methods by editing the camera class definition: + +1. Add the following to the list of imports at the top of hello-world/models/hello-camera.go: + + ```go {class="line-numbers linkable-line-numbers" data-start="6"} + "os" + "reflect" + "io/ioutil" + ``` + +1. Add `imagePath = ""` to the global variables so you have the following: + + ```go {class="line-numbers linkable-line-numbers" data-line="22" data-start="19" data-line-offset="19"} + var ( + HelloCamera = resource.NewModel("jessamy", "hello-world", "hello-camera") + errUnimplemented = errors.New("unimplemented") + imagePath = "" + ) + ``` + +1. In the test script you hard-coded the path to the image. + For the module, let's make the path a configurable attribute so you or other users of the module can set the path from which to get the image. + + Edit the `type Config struct` definition, replacing the comments with the following: + + ```go {class="line-numbers" data-start="33"} + type Config struct { + resource.AlwaysRebuild + ImagePath string `json:"image_path"` + } + ``` + + This adds the `image_path` attribute and causes the resource to rebuild each time the configuration is changed. + +1. We are not providing a default image but rely on the end user to supply a valid path to an image when configuring the resource. + This means `image_path` is a required attribute. + Replace the `Validate` function with the following code to throw an error if `image_path` isn't configured or isn't a string: + + ```go {class="line-numbers linkable-line-numbers" data-start="38"} + func (cfg *Config) Validate(path string) ([]string, error) { + var deps []string + if cfg.ImagePath == "" { + return nil, resource.NewConfigValidationFieldRequiredError(path, "image_path") + } + if reflect.TypeOf(cfg.ImagePath).Kind() != reflect.String { + return nil, errors.New("image_path must be a string.") + } + imagePath = cfg.ImagePath + return deps, nil + } + ``` + +1. The module generator created a stub for the `Image` function we want to implement: + + ```go {class="line-numbers linkable-line-numbers" data-start="103" } + func (s *helloWorldHelloCamera) Image(ctx context.Context, mimeType string, extra map[string]interface{}) ([]byte, camera.ImageMetadata, error) { + panic("not implemented") + } + ``` + + You need to replace `panic("not implemented")` with code to actually implement the method: + + ```go {class="line-numbers linkable-line-numbers" data-start="104" } + imgFile, err := os.Open(imagePath) + if err != nil { + return nil, camera.ImageMetadata{}, errors.New("Error opening image.") + } + defer imgFile.Close() + imgByte, err := ioutil.ReadFile(imagePath) + return imgByte, camera.ImageMetadata{}, nil + ``` + +1. Delete the `SubscribeRTP` and `Unsubscribe` methods, since they are not applicable to this camera. + +1. You can leave the rest of the functions not implemented, because this module is not meant to return a point cloud (`NextPointCloud`), and does not need to return multiple images simultaneously (`Images`). + If this camera returned a camera stream instead of a single static file, we would have implemented `Stream` instead of `Read`. + + However, you do need to edit the return statements to return empty structs that match the API. + Edit these methods so they look like this: + + ```go {class="line-numbers linkable-line-numbers" data-start="110" } + func (s *helloWorldHelloCamera) NewClientFromConn(ctx context.Context, conn rpc.ClientConn, remoteName string, name resource.Name, logger logging.Logger) (camera.Camera, error) { + return nil, errors.New("not implemented") + } + + func (s *helloWorldHelloCamera) Stream(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { + return nil, errors.New("not implemented") + } + + func (s *helloWorldHelloCamera) Images(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { + return []camera.NamedImage{}, resource.ResponseMetadata{}, errors.New("not implemented") + } + + func (s *helloWorldHelloCamera) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { + return nil, errors.New("not implemented") + } + + func (s *helloWorldHelloCamera) Properties(ctx context.Context) (camera.Properties, error) { + return camera.Properties{}, errors.New("not implemented") + } + + func (s *helloWorldHelloCamera) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { + return map[string]interface{}{}, errors.New("not implemented") + } + ``` + +1. Save the file. + +### Implement the sensor API + +{{< expand "Click if you are also creating a sensor component" >}} + +Now edit the sensor class definition to implement the sensor API. +You don't need to edit any of the validate or configuration methods because you're not adding any configurable attributes for the sensor model. + +1. Add `"math/rand"` to the list of imports in hello-sensor.go for the random number generation.

+ +1. Since `errUnimplemented` and `Config` are defined in hello-camera.go, you need to change hello-sensor.go to avoid redeclaring them:

+ + - Delete line 16, `errUnimplemented = errors.New("unimplemented")` from hello-sensor.go.

+ + - On line 27, change `type Config struct {` to `type sensorConfig struct {`.

+ + - Search for all instances of `*Config` in hello-sensor.go and change them to `*sensorConfig`. + +1. The sensor API only has one resource-specific method, `Readings`: + + ```go {class="line-numbers linkable-line-numbers" data-start="93" } + func (s *helloWorldHelloSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { + panic("not implemented") + } + ``` + + Replace `panic("not implemented")` with the following code: + + ```go {class="line-numbers linkable-line-numbers" data-start="94" } + number := rand.Float64() + return map[string]interface{}{ + "random_number": number, + }, nil + ``` + +1. In the `NewClientFromConn` definition, replace `panic("not implemented")` with the following: + + ```go {class="line-numbers linkable-line-numbers" data-start="90"} + return nil, errUnimplemented + ``` + +1. In the `DoCommand` definition, replace `panic("not implemented")` with the following: + + ```go {class="line-numbers linkable-line-numbers" data-start="101"} + return map[string]interface{}{}, errors.New("not implemented") + ``` + +1. Save the file. + +{{< /expand >}} + {{% /tab %}} {{< /tabs >}} @@ -397,6 +662,9 @@ You don't need to edit any of the validate or configuration methods because you' With the implementation written, it's time to test your module locally: +{{< tabs >}} +{{% tab name="Python" %}} + 1. 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="$"} @@ -406,7 +674,20 @@ With the implementation written, it's time to test your module locally: This environment is where the local module will run. `viam-server` does not need to run inside this environment. -1. Make sure your machine's instance of `viam-server` is live and connected to the [Viam app](https://app.viam.com). +{{% /tab %}} +{{% tab name="Go" %}} + +1. From within the hello-world directory, compile your module into a single executable: + + ```sh {class="command-line" data-prompt="$" data-output="5-10"} + make setup + make build + ``` + +{{% /tab %}} +{{< /tabs >}} + +2. Make sure your machine's instance of `viam-server` is live and connected to the [Viam app](https://app.viam.com). 1. In the Viam app, navigate to your machine's **CONFIGURE** page. @@ -478,7 +759,10 @@ The hello world module you created is for learning purposes, not to provide any {{< /expand >}} -To package and upload your module and make it available to configure on machines in your organization: +To package (for Python) and upload your module and make it available to configure on machines in your organization: + +{{< tabs >}} +{{% tab name="Python" %}} 1. Package the module as an archive, run the following command from inside the hello-world directory: @@ -494,10 +778,23 @@ To package and upload your module and make it available to configure on machines viam module upload --version 1.0.0 --platform any module.tar.gz ``` -1. Now, if you look at the [Viam Registry page](https://app.viam.com/registry) while logged into your account, you'll be able to find your private module listed. - You can configure the hello-sensor and hello-camera on your machines just as you would configure other components and services; there's no more need for local module configuration. +{{% /tab %}} +{{% tab name="Go" %}} + +From within your hello-world directory, run the `viam module upload` CLI command to upload the module to the registry: + +```sh {id="terminal-prompt" class="command-line" data-prompt="$"} +viam module upload --version 1.0.0 --platform any . +``` + +{{% /tab %}} +{{< /tabs >}} + +Now, if you look at the [Viam Registry page](https://app.viam.com/registry) while logged into your account, you'll be able to find your private module listed. +Because the module is now in the registry, you can configure the hello-sensor and hello-camera on your machines just as you would configure other components and services; there's no more need for local module configuration. +The local module configuration is primarily for testing purposes. - ![The create a component menu open, searching for hello. The hello-camera and hello-sensor components are shown in the search results.](/how-tos/hello-config.png) +![The create a component menu open, searching for hello. The hello-camera and hello-sensor components are shown in the search results.](/how-tos/hello-config.png) For more information about uploading modules, see [Upload a module](/how-tos/upload-module/). diff --git a/docs/how-tos/upload-module.md b/docs/how-tos/upload-module.md index 5f580f555c..5894f3b9e4 100644 --- a/docs/how-tos/upload-module.md +++ b/docs/how-tos/upload-module.md @@ -111,7 +111,7 @@ Edit the newly-created `meta.json` file, and provide the required configuration module_id string Required - The module ID, which includes either the module namespace or organization-id, followed by its name (pre-populated using the --name you provided in the viam module create command). + The module ID, which includes either the module namespace or organization ID, followed by its name (pre-populated using the --name you provided in the viam module create command).