From 8ae2013585f3691811cbe721ce5f8de230ed07d9 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 27 Oct 2025 13:46:56 +0100 Subject: [PATCH 1/8] add useByApps field for brick details endpoint --- cmd/arduino-app-cli/brick/bricks.go | 6 +- cmd/arduino-app-cli/brick/details.go | 10 +-- cmd/arduino-app-cli/main.go | 2 +- internal/api/api.go | 2 +- internal/api/handlers/bricks.go | 6 +- internal/orchestrator/bricks/bricks.go | 85 +++++++++++++++++++++++++- 6 files changed, 100 insertions(+), 11 deletions(-) diff --git a/cmd/arduino-app-cli/brick/bricks.go b/cmd/arduino-app-cli/brick/bricks.go index 692552cc..74dd3a3a 100644 --- a/cmd/arduino-app-cli/brick/bricks.go +++ b/cmd/arduino-app-cli/brick/bricks.go @@ -17,16 +17,18 @@ package brick import ( "github.com/spf13/cobra" + + "github.com/arduino/arduino-app-cli/internal/orchestrator/config" ) -func NewBrickCmd() *cobra.Command { +func NewBrickCmd(cfg config.Configuration) *cobra.Command { appCmd := &cobra.Command{ Use: "brick", Short: "Manage Arduino Bricks", } appCmd.AddCommand(newBricksListCmd()) - appCmd.AddCommand(newBricksDetailsCmd()) + appCmd.AddCommand(newBricksDetailsCmd(cfg)) return appCmd } diff --git a/cmd/arduino-app-cli/brick/details.go b/cmd/arduino-app-cli/brick/details.go index fe025078..a1ba72f1 100644 --- a/cmd/arduino-app-cli/brick/details.go +++ b/cmd/arduino-app-cli/brick/details.go @@ -25,21 +25,23 @@ import ( "github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/internal/servicelocator" "github.com/arduino/arduino-app-cli/cmd/feedback" "github.com/arduino/arduino-app-cli/internal/orchestrator/bricks" + "github.com/arduino/arduino-app-cli/internal/orchestrator/config" ) -func newBricksDetailsCmd() *cobra.Command { +func newBricksDetailsCmd(cfg config.Configuration) *cobra.Command { return &cobra.Command{ Use: "details", Short: "Details of a specific brick", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - bricksDetailsHandler(args[0]) + bricksDetailsHandler(args[0], cfg) }, } } -func bricksDetailsHandler(id string) { - res, err := servicelocator.GetBrickService().BricksDetails(id) +func bricksDetailsHandler(id string, cfg config.Configuration) { + res, err := servicelocator.GetBrickService().BricksDetails(id, servicelocator.GetAppIDProvider(), + cfg) if err != nil { if errors.Is(err, bricks.ErrBrickNotFound) { feedback.Fatal(err.Error(), feedback.ErrBadArgument) diff --git a/cmd/arduino-app-cli/main.go b/cmd/arduino-app-cli/main.go index 765ccd69..e859aae2 100644 --- a/cmd/arduino-app-cli/main.go +++ b/cmd/arduino-app-cli/main.go @@ -71,7 +71,7 @@ func run(configuration cfg.Configuration) error { rootCmd.AddCommand( app.NewAppCmd(configuration), - brick.NewBrickCmd(), + brick.NewBrickCmd(configuration), completion.NewCompletionCommand(), daemon.NewDaemonCmd(configuration, Version), properties.NewPropertiesCmd(configuration), diff --git a/internal/api/api.go b/internal/api/api.go index 1d825317..08d31d84 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -56,7 +56,7 @@ func NewHTTPRouter( mux.Handle("GET /v1/version", handlers.HandlerVersion(version)) mux.Handle("GET /v1/config", handlers.HandleConfig(cfg)) mux.Handle("GET /v1/bricks", handlers.HandleBrickList(brickService)) - mux.Handle("GET /v1/bricks/{brickID}", handlers.HandleBrickDetails(brickService)) + mux.Handle("GET /v1/bricks/{brickID}", handlers.HandleBrickDetails(brickService, idProvider, cfg)) mux.Handle("GET /v1/properties", handlers.HandlePropertyKeys(cfg)) mux.Handle("GET /v1/properties/{key}", handlers.HandlePropertyGet(cfg)) diff --git a/internal/api/handlers/bricks.go b/internal/api/handlers/bricks.go index d21c6b3a..7e95753a 100644 --- a/internal/api/handlers/bricks.go +++ b/internal/api/handlers/bricks.go @@ -26,6 +26,7 @@ import ( "github.com/arduino/arduino-app-cli/internal/api/models" "github.com/arduino/arduino-app-cli/internal/orchestrator/app" "github.com/arduino/arduino-app-cli/internal/orchestrator/bricks" + "github.com/arduino/arduino-app-cli/internal/orchestrator/config" "github.com/arduino/arduino-app-cli/internal/render" ) @@ -153,14 +154,15 @@ func HandleBrickCreate( } } -func HandleBrickDetails(brickService *bricks.Service) http.HandlerFunc { +func HandleBrickDetails(brickService *bricks.Service, idProvider *app.IDProvider, + cfg config.Configuration) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("brickID") if id == "" { render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "id must be set"}) return } - res, err := brickService.BricksDetails(id) + res, err := brickService.BricksDetails(id, idProvider, cfg) if err != nil { if errors.Is(err, bricks.ErrBrickNotFound) { details := fmt.Sprintf("brick with id %q not found", id) diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index 4e08b2c2..461d5848 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -18,6 +18,7 @@ package bricks import ( "errors" "fmt" + "log/slog" "maps" "slices" @@ -26,6 +27,7 @@ import ( "github.com/arduino/arduino-app-cli/internal/orchestrator/app" "github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex" + "github.com/arduino/arduino-app-cli/internal/orchestrator/config" "github.com/arduino/arduino-app-cli/internal/orchestrator/modelsindex" "github.com/arduino/arduino-app-cli/internal/store" ) @@ -125,7 +127,8 @@ func (s *Service) AppBrickInstanceDetails(a *app.ArduinoApp, brickID string) (Br }, nil } -func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) { +func (s *Service) BricksDetails(id string, idProvider *app.IDProvider, + cfg config.Configuration) (BrickDetailsResult, error) { brick, found := s.bricksIndex.FindBrickByID(id) if !found { return BrickDetailsResult{}, ErrBrickNotFound @@ -160,6 +163,20 @@ func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) { } }) + /*qui mi serve una funzione per calcolare questo. devo iterare su ogni app, di esempio o no. + ho già la funzione appList che mi può aiutare. + e per ogni elemento se ho il bircikc id corrente + mi creo un AppReference*/ + appList, err := getAppList(cfg) + if err != nil { + slog.Error("unable to get app list", slog.String("error", err.Error())) + return BrickDetailsResult{}, fmt.Errorf("unable to get app list: %w", err) + } + usedByApps, err := getUsedByApps(appList, brick.ID, idProvider) + if err != nil { + slog.Error("unable to get used by apps", slog.String("error", err.Error())) + return BrickDetailsResult{}, fmt.Errorf("unable to get used by apps: %w", err) + } return BrickDetailsResult{ ID: id, Name: brick.Name, @@ -171,9 +188,75 @@ func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) { Readme: readme, ApiDocsPath: apiDocsPath, CodeExamples: codeExamples, + UsedByApps: usedByApps, }, nil } +func getAppList( + cfg config.Configuration, +) ([]app.ArduinoApp, error) { + var ( + pathsToExplore paths.PathList + appPaths paths.PathList + ) + pathsToExplore.Add(cfg.ExamplesDir()) + pathsToExplore.Add(cfg.AppsDir()) + arduinoApps := []app.ArduinoApp{} + + for _, p := range pathsToExplore { + res, err := p.ReadDirRecursiveFiltered(func(file *paths.Path) bool { + if file.Base() == ".cache" { + return false + } + if file.Join("app.yaml").NotExist() && file.Join("app.yml").NotExist() { + return true + } + return false + }, paths.FilterDirectories(), paths.FilterOutNames("python", "sketch", ".cache")) + + if err != nil { + slog.Error("unable to list apps", slog.String("error", err.Error())) + return arduinoApps, err + } + appPaths.AddAllMissing(res) + } + + for _, file := range appPaths { + app, err := app.Load(file.String()) + if err != nil { + /* result.BrokenApps = append(result.BrokenApps, orchestrator.BrokenAppInfo{ + Name: file.Base(), + Error: fmt.Sprintf("unable to parse the app.yaml: %s", err.Error()), + })*/ + continue + } + + arduinoApps = append(arduinoApps, app) + } + return arduinoApps, nil +} + +func getUsedByApps(apps []app.ArduinoApp, brickId string, idProvider *app.IDProvider) ([]AppReference, error) { + usedByApps := []AppReference{} + for _, app := range apps { + for _, b := range app.Descriptor.Bricks { + if b.ID == brickId { + id, err := idProvider.IDFromPath(app.FullPath) + if err != nil { + return usedByApps, fmt.Errorf("failed to get app ID for %s: %w", app.Name, err) + } + usedByApps = append(usedByApps, AppReference{ + Name: app.Name, + ID: id.String(), + Icon: app.Descriptor.Icon, + }) + break + } + } + } + return usedByApps, nil +} + type BrickCreateUpdateRequest struct { ID string `json:"-"` Model *string `json:"model"` From 6d0e1d96ecb21656af7ef7f132dea012678af2a0 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Tue, 28 Oct 2025 14:19:31 +0100 Subject: [PATCH 2/8] partial test implementation --- internal/orchestrator/bricks/bricks.go | 4 - internal/orchestrator/bricks/bricks_test.go | 230 ++++++++++++ .../arduino/app_bricks/arduino_cloud/API.md | 48 +++ .../testdata/assets/0.4.8/bricks-list.yaml | 330 ++++++++++++++++++ .../docs/arduino/arduino_cloud/README.md | 44 +++ .../arduino/arduino_cloud/1_led_blink.py | 25 ++ .../2_light_with_colors_monitor.py | 21 ++ .../3_light_with_colors_command.py | 31 ++ .../testdata/examples/cloud-blink/README.md | 81 +++++ .../testdata/examples/cloud-blink/app.yaml | 6 + .../examples/cloud-blink/python/main.py | 22 ++ .../examples/cloud-blink/sketch/sketch.ino | 19 + .../examples/cloud-blink/sketch/sketch.yaml | 11 + 13 files changed, 868 insertions(+), 4 deletions(-) create mode 100644 internal/orchestrator/bricks/bricks_test.go create mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/api-docs/arduino/app_bricks/arduino_cloud/API.md create mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/bricks-list.yaml create mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/docs/arduino/arduino_cloud/README.md create mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/1_led_blink.py create mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/2_light_with_colors_monitor.py create mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/3_light_with_colors_command.py create mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/README.md create mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/app.yaml create mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/python/main.py create mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.ino create mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.yaml diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index 461d5848..ac8526ba 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -163,10 +163,6 @@ func (s *Service) BricksDetails(id string, idProvider *app.IDProvider, } }) - /*qui mi serve una funzione per calcolare questo. devo iterare su ogni app, di esempio o no. - ho già la funzione appList che mi può aiutare. - e per ogni elemento se ho il bircikc id corrente - mi creo un AppReference*/ appList, err := getAppList(cfg) if err != nil { slog.Error("unable to get app list", slog.String("error", err.Error())) diff --git a/internal/orchestrator/bricks/bricks_test.go b/internal/orchestrator/bricks/bricks_test.go new file mode 100644 index 00000000..38a77961 --- /dev/null +++ b/internal/orchestrator/bricks/bricks_test.go @@ -0,0 +1,230 @@ +package bricks + +import ( + "os" + "testing" + + "github.com/arduino/go-paths-helper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/arduino/arduino-app-cli/internal/orchestrator/app" + "github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex" + "github.com/arduino/arduino-app-cli/internal/orchestrator/config" + "github.com/arduino/arduino-app-cli/internal/store" +) + +func TestBricksDetails________todo___________(t *testing.T) { + basedir := paths.New("testdata", "assets", "0.4.8").String() + service := setupTestService(t, basedir) + testDataAssetsPath := paths.New(basedir) + + testDir := paths.New("testdata") + t.Setenv("ARDUINO_APP_CLI__APPS_DIR", testDir.Join("apps").String()) + t.Setenv("ARDUINO_APP_CLI__CONFIG_DIR", testDir.Join("config").String()) + t.Setenv("ARDUINO_APP_CLI__DATA_DIR", testDir.String()) + + cfg, err := config.NewFromEnv() + require.NoError(t, err) + idProvider := app.NewAppIDProvider(cfg) + + expectedVars := map[string]BrickVariable{ + "ARDUINO_DEVICE_ID": { + DefaultValue: "", + Description: "Arduino Cloud Device ID", + Required: true, + }, + "ARDUINO_SECRET": { + DefaultValue: "", + Description: "Arduino Cloud Secret", + Required: true, + }, + } + + readmePath := testDataAssetsPath.Join("docs", "arduino", "arduino_cloud", "README.md") + expectedReadmeBytes, err := os.ReadFile(readmePath.String()) + require.NoError(t, err, "Failed to read test readme file") + expectedReadme := string(expectedReadmeBytes) + expectedAPIPath := testDataAssetsPath.Join("api-docs", "arduino", "app_bricks", "arduino_cloud", "API.md").String() + examplesBasePath := testDataAssetsPath.Join("examples", "arduino", "arduino_cloud") + expectedExamples := []CodeExample{ + {Path: examplesBasePath.Join("1_led_blink.py").String()}, + {Path: examplesBasePath.Join("2_light_with_colors_monitor.py").String()}, + {Path: examplesBasePath.Join("3_light_with_colors_command.py").String()}, + } + expectedUsedByApps := []AppReference{ + {ID: "L2hvbWUvbWlya29jcm9idS9hcmR1aW5vX3Byb2plY3RzL2FyZHVpbm8tYXBwLWNsaS9pbnRlcm5hbC9vcmNoZXN0cmF0b3IvYnJpY2tzL3Rlc3RkYXRhL2V4YW1wbGVzL2Nsb3VkLWJsaW5r", + Name: "Blinking LED from Arduino Cloud", + Icon: "☁️", + }, + } + + testCases := []struct { + name string + brickID string + wantErr bool + wantErrMsg string + expectedResult BrickDetailsResult + }{ + { + name: "Success - brick found", + brickID: "arduino:arduino_cloud", + wantErr: false, + expectedResult: BrickDetailsResult{ + ID: "arduino:arduino_cloud", + Name: "Arduino Cloud", + Author: "Arduino", + Description: "Connects to Arduino Cloud", + Category: "", + Status: "installed", + Variables: expectedVars, + Readme: expectedReadme, + ApiDocsPath: expectedAPIPath, + CodeExamples: expectedExamples, + UsedByApps: expectedUsedByApps, + }, + }, + { + name: "Error - brick not found", + brickID: "arduino:non_existing_brick", + wantErr: true, + wantErrMsg: "brick not found", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := service.BricksDetails(tc.brickID, idProvider, cfg) + + if tc.wantErr { + require.Error(t, err) + if tc.wantErrMsg != "" { + require.Contains(t, err.Error(), tc.wantErrMsg) + } + assert.Equal(t, BrickDetailsResult{}, result) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectedResult, result) + }) + } +} + +func TestBricksDetails(t *testing.T) { + + baseDir := paths.New("testdata", "assets", "0.4.8").String() + service := setupTestService(t, baseDir) + testDataAssetsPath := paths.New(baseDir) + + cfg, err := config.NewFromEnv() + require.NoError(t, err) + idProvider := app.NewAppIDProvider(cfg) + + testCases := []struct { + name string + brickID string + wantErr bool + wantErrMsg string + expectedResult BrickDetailsResult + }{ + { + name: "Success - brick found", + brickID: "arduino:arduino_cloud", + wantErr: false, + expectedResult: BrickDetailsResult{ + ID: "arduino:arduino_cloud", + Name: "Arduino Cloud", + Author: "Arduino", + Description: "Connects to Arduino Cloud", + Category: "", + Status: "installed", + Variables: map[string]BrickVariable{ + "ARDUINO_DEVICE_ID": { + DefaultValue: "", + Description: "Arduino Cloud Device ID", + Required: false, + }, + "ARDUINO_SECRET": { + DefaultValue: "", + Description: "Arduino Cloud Secret", + Required: false, + }, + }, + Readme: string(mustReadFile(t, testDataAssetsPath.Join( + "docs", "arduino", "arduino_cloud", "README.md", + ).String())), + ApiDocsPath: testDataAssetsPath.Join( + "api-docs", "arduino", "app_bricks", "arduino_cloud", "API.md", + ).String(), + CodeExamples: []CodeExample{ + {Path: testDataAssetsPath.Join("examples", "arduino", "arduino_cloud", "1_led_blink.py").String()}, + {Path: testDataAssetsPath.Join("examples", "arduino", "arduino_cloud", "2_light_with_colors_monitor.py").String()}, + {Path: testDataAssetsPath.Join("examples", "arduino", "arduino_cloud", "3_light_with_colors_command.py").String()}, + }, + }, + }, + { + name: "Error - brick not found", + brickID: "arduino:non_existing_brick", + wantErr: true, + wantErrMsg: "brick not found", + }, + { + name: "Success - brick with nil examples", + brickID: "arduino:streamlit_ui", + wantErr: false, + expectedResult: BrickDetailsResult{ + ID: "arduino:streamlit_ui", + Name: "WebUI - Streamlit", + Author: "Arduino", + Description: "A simplified user interface based on Streamlit and Python.", + Category: "ui", + Status: "installed", + Variables: map[string]BrickVariable{}, + Readme: string(mustReadFile(t, testDataAssetsPath.Join( + "docs", "arduino", "streamlit_ui", "README.md", + ).String())), + ApiDocsPath: testDataAssetsPath.Join( + "api-docs", "arduino", "app_bricks", "streamlit_ui", "API.md", + ).String(), + CodeExamples: []CodeExample{}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := service.BricksDetails(tc.brickID, idProvider, cfg) + + if tc.wantErr { + // --- Error Case --- + require.Error(t, err) + if tc.wantErrMsg != "" { + require.Contains(t, err.Error(), tc.wantErrMsg) + } + assert.Equal(t, BrickDetailsResult{}, result) + return + } + + // --- Success Case --- + require.NoError(t, err) + assert.Equal(t, tc.expectedResult, result) + }) + } +} + +func setupTestService(t *testing.T, baseDir string) *Service { + store := store.NewStaticStore(baseDir) + + bricksIndex, err := bricksindex.GenerateBricksIndexFromFile(paths.New(baseDir)) + require.NoError(t, err) + + service := NewService(nil, bricksIndex, store) + return service +} + +func mustReadFile(t *testing.T, path string) []byte { + t.Helper() + bytes, err := os.ReadFile(path) + require.NoError(t, err, "failed to read test file: %s", path) + return bytes +} diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/api-docs/arduino/app_bricks/arduino_cloud/API.md b/internal/orchestrator/bricks/testdata/assets/0.4.8/api-docs/arduino/app_bricks/arduino_cloud/API.md new file mode 100644 index 00000000..8023f784 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/assets/0.4.8/api-docs/arduino/app_bricks/arduino_cloud/API.md @@ -0,0 +1,48 @@ +# arduino_cloud API Reference + +## Index + +- Class `ArduinoCloud` + +--- + +## `ArduinoCloud` class + +```python +class ArduinoCloud(device_id: str, secret: str, server: str, port: int) +``` + +Arduino Cloud client for managing devices and data. + +### Parameters + +- **device_id** (*str*): The unique identifier for the device. +If omitted, uses ARDUINO_DEVICE_ID environment variable. +- **secret** (*str*): The password for Arduino Cloud authentication. +If omitted, uses ARDUINO_SECRET environment variable. +- **server** (*str*) (optional): The server address for Arduino Cloud (default: "iot.arduino.cc"). +- **port** (*int*) (optional): The port to connect to the Arduino Cloud server (default: 8884). + +### Raises + +- **ValueError**: If either device_id or secret is not provided explicitly or via environment variable. + +### Methods + +#### `start()` + +Start the Arduino IoT Cloud client. + +#### `loop()` + +Run a single iteration of the Arduino IoT Cloud client loop, processing commands and updating state. + +#### `register(aiotobj: str | Any)` + +Register a variable or object with the Arduino Cloud client. + +##### Parameters + +- **aiotobj** (*str | Any*): The variable name or object from which to derive the variable name to register. +- ****kwargs** (*Any*): Additional keyword arguments for registration. + diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/bricks-list.yaml b/internal/orchestrator/bricks/testdata/assets/0.4.8/bricks-list.yaml new file mode 100644 index 00000000..a4747e86 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/assets/0.4.8/bricks-list.yaml @@ -0,0 +1,330 @@ +bricks: +- id: arduino:dbstorage_sqlstore + name: Database - SQL + description: Simplified database storage layer for Arduino sensor data using SQLite + local database. + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: [] + category: storage +- id: arduino:object_detection + name: Object Detection + description: "Brick for object detection using a pre-trained model. It processes\ + \ images and returns the predicted class label, bounding-boxes and confidence\ + \ score.\nBrick is designed to work with pre-trained models provided by framework\ + \ or with custom object detection models trained on Edge Impulse platform. \n" + require_container: true + require_model: true + require_devices: false + mount_devices_into_container: false + ports: [] + category: video + model_name: yolox-object-detection + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models + description: path to the custom model directory + - name: EI_OBJ_DETECTION_MODEL + default_value: /models/ootb/ei/yolo-x-nano.eim + description: path to the model file +- id: arduino:mood_detector + name: Mood Detection + description: 'This brick analyzes text sentiment to detect the mood expressed. + + It classifies text as positive, negative, or neutral. + + ' + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: [] + category: text +- id: arduino:camera_code_detection + name: Camera Code Detection + description: Scans a camera for barcodes and QR codes + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: [] + category: video + required_devices: + - camera +- id: arduino:audio_classification + name: Audio Classification + description: 'Brick for audio classification using a pre-trained model. It processes + audio input to classify different sounds. + + Brick is designed to work with pre-trained models provided by framework or with + custom audio classification models trained on Edge Impulse platform. + + ' + require_container: true + require_model: true + require_devices: false + mount_devices_into_container: false + ports: [] + category: audio + model_name: glass-breaking + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models + description: path to the custom model directory + - name: EI_AUDIO_CLASSIFICATION_MODEL + default_value: /models/ootb/ei/glass-breaking.eim + description: path to the model file +- id: arduino:arduino_cloud + name: Arduino Cloud + description: Connects to Arduino Cloud + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: [] + category: null + variables: + - name: ARDUINO_DEVICE_ID + description: Arduino Cloud Device ID + - name: ARDUINO_SECRET + description: Arduino Cloud Secret +- id: arduino:image_classification + name: Image Classification + description: "Brick for image classification using a pre-trained model. It processes\ + \ images and returns the predicted class label and confidence score.\nBrick is\ + \ designed to work with pre-trained models provided by framework or with custom\ + \ image classification models trained on Edge Impulse platform. \n" + require_container: true + require_model: true + require_devices: false + mount_devices_into_container: false + ports: [] + category: video + model_name: mobilenet-image-classification + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models + description: path to the custom model directory + - name: EI_CLASSIFICATION_MODEL + default_value: /models/ootb/ei/mobilenet-v2-224px.eim + description: path to the model file +- id: arduino:streamlit_ui + name: WebUI - Streamlit + description: A simplified user interface based on Streamlit and Python. + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: + - 7000 + category: ui + requires_display: webview +- id: arduino:vibration_anomaly_detection + name: Vibration Anomaly detection + description: 'This Brick is designed for vibration anomaly detection and recognition, + leveraging pre-trained models. + + It takes input from sensors (accelerometer) to identify possible anomalies based + on vibration patterns. + + You can use it with pre-trained models provided by the framework or with your + own custom anomaly detections models trained on the Edge Impulse platform. + + ' + require_container: true + require_model: true + require_devices: false + mount_devices_into_container: false + ports: [] + category: null + model_name: fan-anomaly-detection + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models + description: path to the custom model directory + - name: CUSTOM_MODEL_PATH + default_value: /models/custom/ei/ + description: path to the custom model directory + - name: EI_VIBRATION_ANOMALY_DETECTION_MODEL + default_value: /models/ootb/ei/fan-anomaly-detection.eim +- id: arduino:web_ui + name: WebUI - HTML + description: A user interface based on HTML and JavaScript that can rely on additional + APIs and a WebSocket exposed by a web server. + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: + - 7000 + category: ui + requires_display: webview +- id: arduino:keyword_spotting + name: Keyword Spotting + description: 'Brick for keyword spotting using a pre-trained model. It processes + audio input to detect specific keywords or phrases. + + Brick is designed to work with pre-trained models provided by framework or with + custom audio classification models trained on Edge Impulse platform. + + ' + require_container: true + require_model: true + require_devices: false + mount_devices_into_container: false + ports: [] + category: audio + model_name: keyword-spotting-hey-arduino + required_devices: + - microphone + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models + description: path to the custom model directory + - name: EI_KEYWORD_SPOTTING_MODEL + default_value: /models/ootb/ei/keyword-spotting-hey-arduino.eim + description: path to the model file +- id: arduino:video_image_classification + name: Video Image Classification + description: 'This image classification brick utilizes a pre-trained model to analyze + video streams from a camera. + + It identifies objects, returning their predicted class labels and confidence scores. + + The output is a video stream featuring classification as overaly, with the added + capability to trigger actions based on these detections. + + It supports pre-trained models provided by the framework and custom object detection + models trained on the Edge Impulse platform. + + ' + require_container: true + require_model: true + require_devices: true + mount_devices_into_container: true + ports: [] + category: video + model_name: mobilenet-image-classification + required_devices: + - camera + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models/ + description: path to the custom model directory + - name: EI_CLASSIFICATION_MODEL + default_value: /models/ootb/ei/mobilenet-v2-224px.eim + description: path to the model file + - name: VIDEO_DEVICE + default_value: /dev/video1 +- id: arduino:weather_forecast + name: Weather Forecast + description: Online weather forecast module for Arduino using open-meteo.com geolocation + and weather APIs. Requires an internet connection. + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: [] + category: miscellaneous +- id: arduino:motion_detection + name: Motion detection + description: 'This Brick is designed for motion detection and recognition, leveraging + pre-trained models. + + It takes input from accelerometer sensors to identify various motion patterns. + + You can use it with pre-trained models provided by the framework or with your + custom motion classification models trained on the Edge Impulse platform. + + ' + require_container: true + require_model: true + require_devices: false + mount_devices_into_container: false + ports: [] + category: null + model_name: updown-wave-motion-detection + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models + description: path to the custom model directory + - name: EI_MOTION_DETECTION_MODEL + default_value: /models/ootb/ei/updown-wave-motion-detection.eim + description: path to the model file +- id: arduino:dbstorage_tsstore + name: Database - Time Series + description: Simplified time series database storage layer for Arduino sensor samples + built on top of InfluxDB. + require_container: true + require_model: false + require_devices: false + mount_devices_into_container: false + ports: [] + category: storage + variables: + - name: APP_HOME + default_value: . + - name: DB_PASSWORD + default_value: Arduino15 + description: Database password + - name: DB_USERNAME + default_value: admin + description: Edge Impulse project API key + - name: INFLUXDB_ADMIN_TOKEN + default_value: 392edbf2-b8a2-481f-979d-3f188b2c05f0 + description: InfluxDB admin token +- id: arduino:visual_anomaly_detection + name: Visual Anomaly Detection + description: "Brick for visual anomaly detection using a pre-trained model. It processes\ + \ images to identify unusual patterns and returns detected anomalies with bounding\ + \ boxes. \nSupports pre-trained models provided by the framework or custom anomaly\ + \ detection models trained on the Edge Impulse platform. \n" + require_container: true + require_model: true + require_devices: false + mount_devices_into_container: false + ports: [] + category: image + model_name: concrete-crack-anomaly-detection + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models + description: path to the custom model directory + - name: EI_V_ANOMALY_DETECTION_MODEL + default_value: /models/ootb/ei/concrete-crack-anomaly-detection.eim + description: path to the model file +- id: arduino:video_object_detection + name: Video Object Detection + description: 'This object detection brick utilizes a pre-trained model to analyze + video streams from a camera. + + It identifies objects, returning their predicted class labels, bounding boxes, + and confidence scores. + + The output is a video stream featuring bounding boxes around detected objects, + with the added capability to trigger actions based on these detections. + + It supports pre-trained models provided by the framework and custom object detection + models trained on the Edge Impulse platform. + + ' + require_container: true + require_model: true + require_devices: true + mount_devices_into_container: true + ports: [] + category: null + model_name: yolox-object-detection + required_devices: + - camera + variables: + - name: CUSTOM_MODEL_PATH + default_value: /home/arduino/.arduino-bricks/ei-models/ + description: path to the custom model directory + - name: EI_OBJ_DETECTION_MODEL + default_value: /models/ootb/ei/yolo-x-nano.eim + description: path to the model file + - name: VIDEO_DEVICE + default_value: /dev/video1 diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/docs/arduino/arduino_cloud/README.md b/internal/orchestrator/bricks/testdata/assets/0.4.8/docs/arduino/arduino_cloud/README.md new file mode 100644 index 00000000..00e3ab98 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/assets/0.4.8/docs/arduino/arduino_cloud/README.md @@ -0,0 +1,44 @@ +# Arduino Cloud Brick + +This Brick provides integration with the Arduino Cloud platform, enabling IoT devices to communicate and synchronize data seamlessly. + +## Overview + +The Arduino Cloud Brick simplifies the process of connecting your Arduino device to the Arduino Cloud. It abstracts the complexities of device management, authentication, and data synchronization, allowing developers to focus on building applications and features. With this module, you can easily register devices, exchange data, and leverage cloud-based automation for your projects. + +## Features + +- Connects Arduino devices to the Arduino Cloud +- Supports device registration and authentication +- Enables data exchange between devices and the cloud +- Provides APIs for sending and receiving data + +## Prerequisites + +To use this Brick, we need to have an active Arduino Cloud account, and a **device** and **thing** setup. To obtain the credentials, please follow the instructions at this [link](https://docs.arduino.cc/arduino-cloud/features/manual-device/). This is also covered in the [Blinking LED with Arduino Cloud](/examples/cloud-blink). + +During the device configuration, we will obtain a `device_id` and `secret_key`, which is needed to use this Brick. Note that a Thing with the device associated is required, and that you will need to create variables / dashboard to send and receive data from the board. + +### Adding Credentials + +The `device_id` and `secret_key` can be added inside the Arduino Cloud brick, by clicking on the **Brick Configuration** button inside the Brick. + +Clicking the button will provide two fields where the `device_id` and `secret_key` can be added to the Brick. + +## Code Example and Usage + +```python +from arduino.app_bricks.arduino_cloud import ArduinoCloud +from arduino.app_utils import App, Bridge + +iot_cloud = ArduinoCloud() + +def led_callback(client: object, value: bool): + """Callback function to handle LED blink updates from cloud.""" + print(f"LED blink value updated from cloud: {value}") + Bridge.call("set_led_state", value) + +iot_cloud.register("led", value=False, on_write=led_callback) + +App.run() +``` \ No newline at end of file diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/1_led_blink.py b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/1_led_blink.py new file mode 100644 index 00000000..58cd9470 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/1_led_blink.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA +# +# SPDX-License-Identifier: MPL-2.0 + +# EXAMPLE_NAME = "Arduino Cloud LED Blink Example" +from arduino.app_bricks.arduino_cloud import ArduinoCloud +from arduino.app_utils import App +import time + +# If secrets are not provided in the class initialization, they will be read from environment variables +arduino_cloud = ArduinoCloud() + + +def led_callback(client: object, value: bool): + """Callback function to handle LED blink updates from cloud.""" + print(f"LED blink value updated from cloud: {value}") + + +arduino_cloud.register("led", value=False, on_write=led_callback) + +App.start_brick(arduino_cloud) +while True: + arduino_cloud.led = not arduino_cloud.led + print(f"LED blink set to: {arduino_cloud.led}") + time.sleep(3) diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/2_light_with_colors_monitor.py b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/2_light_with_colors_monitor.py new file mode 100644 index 00000000..40371db7 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/2_light_with_colors_monitor.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA +# +# SPDX-License-Identifier: MPL-2.0 + +# EXAMPLE_NAME = "Arduino Cloud Light with Colors Example" +from arduino.app_bricks.arduino_cloud import ArduinoCloud, ColoredLight +from arduino.app_utils import App +from typing import Any + +# If secrets are not provided in the class initialization, they will be read from environment variables +arduino_cloud = ArduinoCloud() + + +def light_callback(client: object, value: Any): + """Callback function to handle light updates from cloud.""" + print(f"Light value updated from cloud: {value}") + + +arduino_cloud.register(ColoredLight("clight", swi=True, on_write=light_callback)) + +App.run() diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/3_light_with_colors_command.py b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/3_light_with_colors_command.py new file mode 100644 index 00000000..5e730ea9 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/3_light_with_colors_command.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA +# +# SPDX-License-Identifier: MPL-2.0 + +# EXAMPLE_NAME = "Arduino Cloud Light with Colors Example" +from arduino.app_bricks.arduino_cloud import ArduinoCloud, ColoredLight +from arduino.app_utils import App +from typing import Any +import time +import random + +# If secrets are not provided in the class initialization, they will be read from environment variables +arduino_cloud = ArduinoCloud() + + +def light_callback(client: object, value: Any): + """Callback function to handle light updates from cloud.""" + print(f"Light value updated from cloud: {value}") + + +arduino_cloud.register(ColoredLight("clight", swi=True, on_write=light_callback)) + +App.start_brick(arduino_cloud) + +while True: + # randomize color + arduino_cloud.clight.hue = random.randint(0, 360) + arduino_cloud.clight.sat = random.randint(0, 100) + arduino_cloud.clight.bri = random.randint(0, 100) + print(f"Light set to hue: {arduino_cloud.clight.hue}, saturation: {arduino_cloud.clight.sat}, brightness: {arduino_cloud.clight.bri}") + time.sleep(3) diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/README.md b/internal/orchestrator/bricks/testdata/examples/cloud-blink/README.md new file mode 100644 index 00000000..a20d5a09 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/examples/cloud-blink/README.md @@ -0,0 +1,81 @@ +# Blinking LED from Arduino Cloud + +This **Blinking LED from Arduino Cloud** example allows us to remotely control the onboard LED on the Arduino® UNO Q from the [Arduino Cloud](https://app.arduino.cc/). +The LED is controlled by creating a dashboard with a switch in the Arduino Cloud. + +## Bricks Used + +The Blinking LED from Arduino Cloud example uses the following Bricks: + +- `arduino_cloud`: Brick to create a connection to the Arduino Cloud + +## Hardware and Software Requirements + +### Hardware + +- [Arduino® UNO Q](https://store.arduino.cc/products/uno-q) +- [USB-C® cable](https://store.arduino.cc/products/usb-cable2in1-type-c) + +### Software + +- Arduino App Lab +- [Arduino Cloud](https://app.arduino.cc/) + +**Note:** You can run this example using your Arduino® UNO Q as a Single Board Computer (SBC preview mode) using a [USB-C® hub](https://store.arduino.cc/products/usb-c-to-hdmi-multiport-adapter-with-ethernet-and-usb-hub) with a mouse, keyboard and display attached. + +## How to Use the Example + +This example requires an active Arduino Cloud account, with a device, thing and dashboard set up. + +### Setting Up Arduino Cloud + +1. Navigate to the [Arduino Cloud](https://app.arduino.cc/) page and log in / create an account. +2. Go to the [devices](https://app.arduino.cc/devices) page and create a device, selecting the "manual device" type. Follow the instructions and take note of the **device_id** and **secret_key** provided in the setup. + ![Arduino Cloud credentials](assets/docs_images/cloud-blink-device.png) +3. Go to the [things](https://app.arduino.cc/things) page and create a new thing. +4. Inside the thing, create a new **boolean** variable, and name it **"led"**. We also need to associate the device we created with this thing. + ![Arduino Cloud thing](assets/docs_images/cloud-blink-thing.png) +5. Finally, navigate to the [dashboards](), and create a dashboard. Inside the dashboard, click on **"Edit"**, and select the thing we just created. This will automatically assign a switch widget to the **led** variable. + ![Arduino Cloud dashboard](assets/docs_images/cloud-blink-dashboard.png) + +### Configure & Launch App + +1. Duplicate this example, by clicking on the arrow next to the App example name. As we will need to add the credentials, we will need to duplicate it, as we are not able to edit any of the built-in examples. + ![Duplicate example](assets/docs_images/cloud-blink-duplicate.png) + +2. In the Arduino App Lab on this page, go to `app.yaml` file, and add the **device_id** and **secret_key** credentials to the `ARDUINO_DEVICE_ID` and `ARDUINO_SECRET` variables. + ![Add credentials](assets/docs_images/cloud-blink-creds.png) + +3. Launch the App by clicking on the "Play" button in the top right corner. Wait until the App has launched. + ![Launching an App](assets/docs_images/launch-app-cloud-blink.png) + +## How it Works + +The application works by establishing a connection between the Arduino Cloud and the UNO Q board. When interacting with the dashboard's switch widget (turn ON/OFF), the cloud updates the "led" property. + +The `main.py` script running on the Linux system listens for changes to this property using the `arduino_cloud` Brick. When a change is detected, the **Bridge** tool is used to send data to the microcontroller, and turn the LED ON. + +The flow of the App is: +1. The switch in the Arduino Cloud dashboard is changed. +2. The Arduino Cloud updates the device's state. +3. `main.py` receives the updated state, sends a message to the microcontroller which turns the LED to an ON/OFF state. + +![How the Cloud interacts with UNO Q](assets/docs_images/cloud-blink.png) + +### Understanding the Code + +On the Linux (Python®) side: +- `iot_cloud = ArduinoCloud()` - initializes the `ArduinoCloud` class. +- `iot_cloud.register("led", value=False, on_write=led_callback)` - creates a callback function that fires when the value in the Arduino Cloud changes. +- `Bridge.call("set_led_state", value)` - calls the microcontroller with the updated state. + +On the microcontroller (sketch) side: +- `Bridge.provide("set_led_state", set_led_state);` - we receive an update from the Linux (Python®) side, and trigger the `set_led_state()` function. + +The `set_led_state()` function passes the updated state, and turns ON/OFF the LED: + +```arduino +void set_led_state(bool state) { + digitalWrite(LED_BUILTIN, state ? LOW : HIGH); +} +``` diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/app.yaml b/internal/orchestrator/bricks/testdata/examples/cloud-blink/app.yaml new file mode 100644 index 00000000..8af8e098 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/examples/cloud-blink/app.yaml @@ -0,0 +1,6 @@ +name: Blinking LED from Arduino Cloud +icon: ☁️ +description: Control the LED from the Arduino IoT Cloud using RPC calls + +bricks: + - arduino:arduino_cloud \ No newline at end of file diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/python/main.py b/internal/orchestrator/bricks/testdata/examples/cloud-blink/python/main.py new file mode 100644 index 00000000..f5264020 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/examples/cloud-blink/python/main.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA +# +# SPDX-License-Identifier: MPL-2.0 + +# EXAMPLE_NAME = "Arduino Cloud LED Blink Example" +from arduino.app_bricks.arduino_cloud import ArduinoCloud +from arduino.app_utils import App, Bridge + +# If secrets are not provided in the class initialization, they will be read from environment variables +iot_cloud = ArduinoCloud() + + +def led_callback(client: object, value: bool): + """Callback function to handle LED blink updates from cloud.""" + print(f"LED blink value updated from cloud: {value}") + # Call a function in the sketch, using the Bridge helper library, to control the state of the LED connected to the microcontroller. + # This performs a RPC call and allows the Python code and the Sketch code to communicate. + Bridge.call("set_led_state", value) + +iot_cloud.register("led", value=False, on_write=led_callback) + +App.run() diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.ino b/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.ino new file mode 100644 index 00000000..8c7aef66 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.ino @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA +// +// SPDX-License-Identifier: MPL-2.0 + +#include + +void setup() { + pinMode(LED_BUILTIN, OUTPUT); + + Bridge.begin(); + Bridge.provide("set_led_state", set_led_state); +} + +void loop() {} + +void set_led_state(bool state) { + // LOW state means LED is ON + digitalWrite(LED_BUILTIN, state ? LOW : HIGH); +} diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.yaml b/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.yaml new file mode 100644 index 00000000..d9fe917e --- /dev/null +++ b/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.yaml @@ -0,0 +1,11 @@ +profiles: + default: + fqbn: arduino:zephyr:unoq + platforms: + - platform: arduino:zephyr + libraries: + - MsgPack (0.4.2) + - DebugLog (0.8.4) + - ArxContainer (0.7.0) + - ArxTypeTraits (0.3.1) +default_profile: default From bd4147e225c64741d38c88d8458c9d63067c3393 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Wed, 29 Oct 2025 17:55:36 +0100 Subject: [PATCH 3/8] add test end2end --- internal/e2e/daemon/brick_test.go | 44 +++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/internal/e2e/daemon/brick_test.go b/internal/e2e/daemon/brick_test.go index ab04859f..5f0c1eba 100644 --- a/internal/e2e/daemon/brick_test.go +++ b/internal/e2e/daemon/brick_test.go @@ -24,13 +24,44 @@ import ( "github.com/arduino/go-paths-helper" "github.com/stretchr/testify/require" + "go.bug.st/f" "github.com/arduino/arduino-app-cli/internal/api/models" + "github.com/arduino/arduino-app-cli/internal/e2e/client" "github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex" "github.com/arduino/arduino-app-cli/internal/orchestrator/config" "github.com/arduino/arduino-app-cli/internal/store" ) +func setupTesBrick(t *testing.T) (*client.CreateAppResp, *client.ClientWithResponses) { + httpClient := GetHttpclient(t) + createResp, err := httpClient.CreateAppWithResponse( + t.Context(), + &client.CreateAppParams{SkipSketch: f.Ptr(true)}, + client.CreateAppRequest{ + Icon: f.Ptr("💻"), + Name: "test-app", + Description: f.Ptr("My app description"), + }, + func(ctx context.Context, req *http.Request) error { return nil }, + ) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, createResp.StatusCode()) + require.NotNil(t, createResp.JSON201) + + resp, err := httpClient.UpsertAppBrickInstanceWithResponse( + t.Context(), + *createResp.JSON201.Id, + ImageClassifactionBrickID, + client.BrickCreateUpdateRequest{Model: f.Ptr("mobilenet-image-classification")}, + func(ctx context.Context, req *http.Request) error { return nil }, + ) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode()) + + return createResp, httpClient +} + func TestBricksList(t *testing.T) { httpClient := GetHttpclient(t) @@ -56,8 +87,8 @@ func TestBricksList(t *testing.T) { } func TestBricksDetails(t *testing.T) { + _, httpClient := setupTesBrick(t) - httpClient := GetHttpclient(t) t.Run("should return 404 Not Found for an invalid brick ID", func(t *testing.T) { invalidBrickID := "notvalidBrickId" var actualBody models.ErrorResponse @@ -76,6 +107,14 @@ func TestBricksDetails(t *testing.T) { t.Run("should return 200 OK with full details for a valid brick ID", func(t *testing.T) { validBrickID := "arduino:image_classification" + expectedUsedByApps := []client.AppReference{ + { + Id: f.Ptr("dXNlcjp0ZXN0LWFwcA"), + Name: f.Ptr("test-app"), + Icon: f.Ptr("💻"), + }, + } + response, err := httpClient.GetBrickDetailsWithResponse(t.Context(), validBrickID, func(ctx context.Context, req *http.Request) error { return nil }) require.NoError(t, err) require.Equal(t, http.StatusOK, response.StatusCode(), "status code should be 200 ok") @@ -92,6 +131,7 @@ func TestBricksDetails(t *testing.T) { require.Equal(t, "path to the model file", *(*response.JSON200.Variables)["EI_CLASSIFICATION_MODEL"].Description) require.Equal(t, false, *(*response.JSON200.Variables)["EI_CLASSIFICATION_MODEL"].Required) require.NotEmpty(t, *response.JSON200.Readme) - require.Nil(t, response.JSON200.UsedByApps) + require.NotNil(t, response.JSON200.UsedByApps, "UsedByApps should not be nil") + require.Equal(t, expectedUsedByApps, *(response.JSON200.UsedByApps)) }) } From 0f7cbe57f3505da8fb509de90c1c056221b2465d Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Wed, 29 Oct 2025 18:02:10 +0100 Subject: [PATCH 4/8] delete wrong tests --- internal/orchestrator/bricks/bricks_test.go | 230 ------------ .../arduino/app_bricks/arduino_cloud/API.md | 48 --- .../testdata/assets/0.4.8/bricks-list.yaml | 330 ------------------ .../docs/arduino/arduino_cloud/README.md | 44 --- .../arduino/arduino_cloud/1_led_blink.py | 25 -- .../2_light_with_colors_monitor.py | 21 -- .../3_light_with_colors_command.py | 31 -- .../testdata/examples/cloud-blink/README.md | 81 ----- .../testdata/examples/cloud-blink/app.yaml | 6 - .../examples/cloud-blink/python/main.py | 22 -- .../examples/cloud-blink/sketch/sketch.ino | 19 - .../examples/cloud-blink/sketch/sketch.yaml | 11 - 12 files changed, 868 deletions(-) delete mode 100644 internal/orchestrator/bricks/bricks_test.go delete mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/api-docs/arduino/app_bricks/arduino_cloud/API.md delete mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/bricks-list.yaml delete mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/docs/arduino/arduino_cloud/README.md delete mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/1_led_blink.py delete mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/2_light_with_colors_monitor.py delete mode 100644 internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/3_light_with_colors_command.py delete mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/README.md delete mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/app.yaml delete mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/python/main.py delete mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.ino delete mode 100644 internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.yaml diff --git a/internal/orchestrator/bricks/bricks_test.go b/internal/orchestrator/bricks/bricks_test.go deleted file mode 100644 index 38a77961..00000000 --- a/internal/orchestrator/bricks/bricks_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package bricks - -import ( - "os" - "testing" - - "github.com/arduino/go-paths-helper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/arduino/arduino-app-cli/internal/orchestrator/app" - "github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex" - "github.com/arduino/arduino-app-cli/internal/orchestrator/config" - "github.com/arduino/arduino-app-cli/internal/store" -) - -func TestBricksDetails________todo___________(t *testing.T) { - basedir := paths.New("testdata", "assets", "0.4.8").String() - service := setupTestService(t, basedir) - testDataAssetsPath := paths.New(basedir) - - testDir := paths.New("testdata") - t.Setenv("ARDUINO_APP_CLI__APPS_DIR", testDir.Join("apps").String()) - t.Setenv("ARDUINO_APP_CLI__CONFIG_DIR", testDir.Join("config").String()) - t.Setenv("ARDUINO_APP_CLI__DATA_DIR", testDir.String()) - - cfg, err := config.NewFromEnv() - require.NoError(t, err) - idProvider := app.NewAppIDProvider(cfg) - - expectedVars := map[string]BrickVariable{ - "ARDUINO_DEVICE_ID": { - DefaultValue: "", - Description: "Arduino Cloud Device ID", - Required: true, - }, - "ARDUINO_SECRET": { - DefaultValue: "", - Description: "Arduino Cloud Secret", - Required: true, - }, - } - - readmePath := testDataAssetsPath.Join("docs", "arduino", "arduino_cloud", "README.md") - expectedReadmeBytes, err := os.ReadFile(readmePath.String()) - require.NoError(t, err, "Failed to read test readme file") - expectedReadme := string(expectedReadmeBytes) - expectedAPIPath := testDataAssetsPath.Join("api-docs", "arduino", "app_bricks", "arduino_cloud", "API.md").String() - examplesBasePath := testDataAssetsPath.Join("examples", "arduino", "arduino_cloud") - expectedExamples := []CodeExample{ - {Path: examplesBasePath.Join("1_led_blink.py").String()}, - {Path: examplesBasePath.Join("2_light_with_colors_monitor.py").String()}, - {Path: examplesBasePath.Join("3_light_with_colors_command.py").String()}, - } - expectedUsedByApps := []AppReference{ - {ID: "L2hvbWUvbWlya29jcm9idS9hcmR1aW5vX3Byb2plY3RzL2FyZHVpbm8tYXBwLWNsaS9pbnRlcm5hbC9vcmNoZXN0cmF0b3IvYnJpY2tzL3Rlc3RkYXRhL2V4YW1wbGVzL2Nsb3VkLWJsaW5r", - Name: "Blinking LED from Arduino Cloud", - Icon: "☁️", - }, - } - - testCases := []struct { - name string - brickID string - wantErr bool - wantErrMsg string - expectedResult BrickDetailsResult - }{ - { - name: "Success - brick found", - brickID: "arduino:arduino_cloud", - wantErr: false, - expectedResult: BrickDetailsResult{ - ID: "arduino:arduino_cloud", - Name: "Arduino Cloud", - Author: "Arduino", - Description: "Connects to Arduino Cloud", - Category: "", - Status: "installed", - Variables: expectedVars, - Readme: expectedReadme, - ApiDocsPath: expectedAPIPath, - CodeExamples: expectedExamples, - UsedByApps: expectedUsedByApps, - }, - }, - { - name: "Error - brick not found", - brickID: "arduino:non_existing_brick", - wantErr: true, - wantErrMsg: "brick not found", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result, err := service.BricksDetails(tc.brickID, idProvider, cfg) - - if tc.wantErr { - require.Error(t, err) - if tc.wantErrMsg != "" { - require.Contains(t, err.Error(), tc.wantErrMsg) - } - assert.Equal(t, BrickDetailsResult{}, result) - return - } - require.NoError(t, err) - assert.Equal(t, tc.expectedResult, result) - }) - } -} - -func TestBricksDetails(t *testing.T) { - - baseDir := paths.New("testdata", "assets", "0.4.8").String() - service := setupTestService(t, baseDir) - testDataAssetsPath := paths.New(baseDir) - - cfg, err := config.NewFromEnv() - require.NoError(t, err) - idProvider := app.NewAppIDProvider(cfg) - - testCases := []struct { - name string - brickID string - wantErr bool - wantErrMsg string - expectedResult BrickDetailsResult - }{ - { - name: "Success - brick found", - brickID: "arduino:arduino_cloud", - wantErr: false, - expectedResult: BrickDetailsResult{ - ID: "arduino:arduino_cloud", - Name: "Arduino Cloud", - Author: "Arduino", - Description: "Connects to Arduino Cloud", - Category: "", - Status: "installed", - Variables: map[string]BrickVariable{ - "ARDUINO_DEVICE_ID": { - DefaultValue: "", - Description: "Arduino Cloud Device ID", - Required: false, - }, - "ARDUINO_SECRET": { - DefaultValue: "", - Description: "Arduino Cloud Secret", - Required: false, - }, - }, - Readme: string(mustReadFile(t, testDataAssetsPath.Join( - "docs", "arduino", "arduino_cloud", "README.md", - ).String())), - ApiDocsPath: testDataAssetsPath.Join( - "api-docs", "arduino", "app_bricks", "arduino_cloud", "API.md", - ).String(), - CodeExamples: []CodeExample{ - {Path: testDataAssetsPath.Join("examples", "arduino", "arduino_cloud", "1_led_blink.py").String()}, - {Path: testDataAssetsPath.Join("examples", "arduino", "arduino_cloud", "2_light_with_colors_monitor.py").String()}, - {Path: testDataAssetsPath.Join("examples", "arduino", "arduino_cloud", "3_light_with_colors_command.py").String()}, - }, - }, - }, - { - name: "Error - brick not found", - brickID: "arduino:non_existing_brick", - wantErr: true, - wantErrMsg: "brick not found", - }, - { - name: "Success - brick with nil examples", - brickID: "arduino:streamlit_ui", - wantErr: false, - expectedResult: BrickDetailsResult{ - ID: "arduino:streamlit_ui", - Name: "WebUI - Streamlit", - Author: "Arduino", - Description: "A simplified user interface based on Streamlit and Python.", - Category: "ui", - Status: "installed", - Variables: map[string]BrickVariable{}, - Readme: string(mustReadFile(t, testDataAssetsPath.Join( - "docs", "arduino", "streamlit_ui", "README.md", - ).String())), - ApiDocsPath: testDataAssetsPath.Join( - "api-docs", "arduino", "app_bricks", "streamlit_ui", "API.md", - ).String(), - CodeExamples: []CodeExample{}, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result, err := service.BricksDetails(tc.brickID, idProvider, cfg) - - if tc.wantErr { - // --- Error Case --- - require.Error(t, err) - if tc.wantErrMsg != "" { - require.Contains(t, err.Error(), tc.wantErrMsg) - } - assert.Equal(t, BrickDetailsResult{}, result) - return - } - - // --- Success Case --- - require.NoError(t, err) - assert.Equal(t, tc.expectedResult, result) - }) - } -} - -func setupTestService(t *testing.T, baseDir string) *Service { - store := store.NewStaticStore(baseDir) - - bricksIndex, err := bricksindex.GenerateBricksIndexFromFile(paths.New(baseDir)) - require.NoError(t, err) - - service := NewService(nil, bricksIndex, store) - return service -} - -func mustReadFile(t *testing.T, path string) []byte { - t.Helper() - bytes, err := os.ReadFile(path) - require.NoError(t, err, "failed to read test file: %s", path) - return bytes -} diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/api-docs/arduino/app_bricks/arduino_cloud/API.md b/internal/orchestrator/bricks/testdata/assets/0.4.8/api-docs/arduino/app_bricks/arduino_cloud/API.md deleted file mode 100644 index 8023f784..00000000 --- a/internal/orchestrator/bricks/testdata/assets/0.4.8/api-docs/arduino/app_bricks/arduino_cloud/API.md +++ /dev/null @@ -1,48 +0,0 @@ -# arduino_cloud API Reference - -## Index - -- Class `ArduinoCloud` - ---- - -## `ArduinoCloud` class - -```python -class ArduinoCloud(device_id: str, secret: str, server: str, port: int) -``` - -Arduino Cloud client for managing devices and data. - -### Parameters - -- **device_id** (*str*): The unique identifier for the device. -If omitted, uses ARDUINO_DEVICE_ID environment variable. -- **secret** (*str*): The password for Arduino Cloud authentication. -If omitted, uses ARDUINO_SECRET environment variable. -- **server** (*str*) (optional): The server address for Arduino Cloud (default: "iot.arduino.cc"). -- **port** (*int*) (optional): The port to connect to the Arduino Cloud server (default: 8884). - -### Raises - -- **ValueError**: If either device_id or secret is not provided explicitly or via environment variable. - -### Methods - -#### `start()` - -Start the Arduino IoT Cloud client. - -#### `loop()` - -Run a single iteration of the Arduino IoT Cloud client loop, processing commands and updating state. - -#### `register(aiotobj: str | Any)` - -Register a variable or object with the Arduino Cloud client. - -##### Parameters - -- **aiotobj** (*str | Any*): The variable name or object from which to derive the variable name to register. -- ****kwargs** (*Any*): Additional keyword arguments for registration. - diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/bricks-list.yaml b/internal/orchestrator/bricks/testdata/assets/0.4.8/bricks-list.yaml deleted file mode 100644 index a4747e86..00000000 --- a/internal/orchestrator/bricks/testdata/assets/0.4.8/bricks-list.yaml +++ /dev/null @@ -1,330 +0,0 @@ -bricks: -- id: arduino:dbstorage_sqlstore - name: Database - SQL - description: Simplified database storage layer for Arduino sensor data using SQLite - local database. - require_container: false - require_model: false - require_devices: false - mount_devices_into_container: false - ports: [] - category: storage -- id: arduino:object_detection - name: Object Detection - description: "Brick for object detection using a pre-trained model. It processes\ - \ images and returns the predicted class label, bounding-boxes and confidence\ - \ score.\nBrick is designed to work with pre-trained models provided by framework\ - \ or with custom object detection models trained on Edge Impulse platform. \n" - require_container: true - require_model: true - require_devices: false - mount_devices_into_container: false - ports: [] - category: video - model_name: yolox-object-detection - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models - description: path to the custom model directory - - name: EI_OBJ_DETECTION_MODEL - default_value: /models/ootb/ei/yolo-x-nano.eim - description: path to the model file -- id: arduino:mood_detector - name: Mood Detection - description: 'This brick analyzes text sentiment to detect the mood expressed. - - It classifies text as positive, negative, or neutral. - - ' - require_container: false - require_model: false - require_devices: false - mount_devices_into_container: false - ports: [] - category: text -- id: arduino:camera_code_detection - name: Camera Code Detection - description: Scans a camera for barcodes and QR codes - require_container: false - require_model: false - require_devices: false - mount_devices_into_container: false - ports: [] - category: video - required_devices: - - camera -- id: arduino:audio_classification - name: Audio Classification - description: 'Brick for audio classification using a pre-trained model. It processes - audio input to classify different sounds. - - Brick is designed to work with pre-trained models provided by framework or with - custom audio classification models trained on Edge Impulse platform. - - ' - require_container: true - require_model: true - require_devices: false - mount_devices_into_container: false - ports: [] - category: audio - model_name: glass-breaking - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models - description: path to the custom model directory - - name: EI_AUDIO_CLASSIFICATION_MODEL - default_value: /models/ootb/ei/glass-breaking.eim - description: path to the model file -- id: arduino:arduino_cloud - name: Arduino Cloud - description: Connects to Arduino Cloud - require_container: false - require_model: false - require_devices: false - mount_devices_into_container: false - ports: [] - category: null - variables: - - name: ARDUINO_DEVICE_ID - description: Arduino Cloud Device ID - - name: ARDUINO_SECRET - description: Arduino Cloud Secret -- id: arduino:image_classification - name: Image Classification - description: "Brick for image classification using a pre-trained model. It processes\ - \ images and returns the predicted class label and confidence score.\nBrick is\ - \ designed to work with pre-trained models provided by framework or with custom\ - \ image classification models trained on Edge Impulse platform. \n" - require_container: true - require_model: true - require_devices: false - mount_devices_into_container: false - ports: [] - category: video - model_name: mobilenet-image-classification - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models - description: path to the custom model directory - - name: EI_CLASSIFICATION_MODEL - default_value: /models/ootb/ei/mobilenet-v2-224px.eim - description: path to the model file -- id: arduino:streamlit_ui - name: WebUI - Streamlit - description: A simplified user interface based on Streamlit and Python. - require_container: false - require_model: false - require_devices: false - mount_devices_into_container: false - ports: - - 7000 - category: ui - requires_display: webview -- id: arduino:vibration_anomaly_detection - name: Vibration Anomaly detection - description: 'This Brick is designed for vibration anomaly detection and recognition, - leveraging pre-trained models. - - It takes input from sensors (accelerometer) to identify possible anomalies based - on vibration patterns. - - You can use it with pre-trained models provided by the framework or with your - own custom anomaly detections models trained on the Edge Impulse platform. - - ' - require_container: true - require_model: true - require_devices: false - mount_devices_into_container: false - ports: [] - category: null - model_name: fan-anomaly-detection - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models - description: path to the custom model directory - - name: CUSTOM_MODEL_PATH - default_value: /models/custom/ei/ - description: path to the custom model directory - - name: EI_VIBRATION_ANOMALY_DETECTION_MODEL - default_value: /models/ootb/ei/fan-anomaly-detection.eim -- id: arduino:web_ui - name: WebUI - HTML - description: A user interface based on HTML and JavaScript that can rely on additional - APIs and a WebSocket exposed by a web server. - require_container: false - require_model: false - require_devices: false - mount_devices_into_container: false - ports: - - 7000 - category: ui - requires_display: webview -- id: arduino:keyword_spotting - name: Keyword Spotting - description: 'Brick for keyword spotting using a pre-trained model. It processes - audio input to detect specific keywords or phrases. - - Brick is designed to work with pre-trained models provided by framework or with - custom audio classification models trained on Edge Impulse platform. - - ' - require_container: true - require_model: true - require_devices: false - mount_devices_into_container: false - ports: [] - category: audio - model_name: keyword-spotting-hey-arduino - required_devices: - - microphone - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models - description: path to the custom model directory - - name: EI_KEYWORD_SPOTTING_MODEL - default_value: /models/ootb/ei/keyword-spotting-hey-arduino.eim - description: path to the model file -- id: arduino:video_image_classification - name: Video Image Classification - description: 'This image classification brick utilizes a pre-trained model to analyze - video streams from a camera. - - It identifies objects, returning their predicted class labels and confidence scores. - - The output is a video stream featuring classification as overaly, with the added - capability to trigger actions based on these detections. - - It supports pre-trained models provided by the framework and custom object detection - models trained on the Edge Impulse platform. - - ' - require_container: true - require_model: true - require_devices: true - mount_devices_into_container: true - ports: [] - category: video - model_name: mobilenet-image-classification - required_devices: - - camera - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models/ - description: path to the custom model directory - - name: EI_CLASSIFICATION_MODEL - default_value: /models/ootb/ei/mobilenet-v2-224px.eim - description: path to the model file - - name: VIDEO_DEVICE - default_value: /dev/video1 -- id: arduino:weather_forecast - name: Weather Forecast - description: Online weather forecast module for Arduino using open-meteo.com geolocation - and weather APIs. Requires an internet connection. - require_container: false - require_model: false - require_devices: false - mount_devices_into_container: false - ports: [] - category: miscellaneous -- id: arduino:motion_detection - name: Motion detection - description: 'This Brick is designed for motion detection and recognition, leveraging - pre-trained models. - - It takes input from accelerometer sensors to identify various motion patterns. - - You can use it with pre-trained models provided by the framework or with your - custom motion classification models trained on the Edge Impulse platform. - - ' - require_container: true - require_model: true - require_devices: false - mount_devices_into_container: false - ports: [] - category: null - model_name: updown-wave-motion-detection - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models - description: path to the custom model directory - - name: EI_MOTION_DETECTION_MODEL - default_value: /models/ootb/ei/updown-wave-motion-detection.eim - description: path to the model file -- id: arduino:dbstorage_tsstore - name: Database - Time Series - description: Simplified time series database storage layer for Arduino sensor samples - built on top of InfluxDB. - require_container: true - require_model: false - require_devices: false - mount_devices_into_container: false - ports: [] - category: storage - variables: - - name: APP_HOME - default_value: . - - name: DB_PASSWORD - default_value: Arduino15 - description: Database password - - name: DB_USERNAME - default_value: admin - description: Edge Impulse project API key - - name: INFLUXDB_ADMIN_TOKEN - default_value: 392edbf2-b8a2-481f-979d-3f188b2c05f0 - description: InfluxDB admin token -- id: arduino:visual_anomaly_detection - name: Visual Anomaly Detection - description: "Brick for visual anomaly detection using a pre-trained model. It processes\ - \ images to identify unusual patterns and returns detected anomalies with bounding\ - \ boxes. \nSupports pre-trained models provided by the framework or custom anomaly\ - \ detection models trained on the Edge Impulse platform. \n" - require_container: true - require_model: true - require_devices: false - mount_devices_into_container: false - ports: [] - category: image - model_name: concrete-crack-anomaly-detection - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models - description: path to the custom model directory - - name: EI_V_ANOMALY_DETECTION_MODEL - default_value: /models/ootb/ei/concrete-crack-anomaly-detection.eim - description: path to the model file -- id: arduino:video_object_detection - name: Video Object Detection - description: 'This object detection brick utilizes a pre-trained model to analyze - video streams from a camera. - - It identifies objects, returning their predicted class labels, bounding boxes, - and confidence scores. - - The output is a video stream featuring bounding boxes around detected objects, - with the added capability to trigger actions based on these detections. - - It supports pre-trained models provided by the framework and custom object detection - models trained on the Edge Impulse platform. - - ' - require_container: true - require_model: true - require_devices: true - mount_devices_into_container: true - ports: [] - category: null - model_name: yolox-object-detection - required_devices: - - camera - variables: - - name: CUSTOM_MODEL_PATH - default_value: /home/arduino/.arduino-bricks/ei-models/ - description: path to the custom model directory - - name: EI_OBJ_DETECTION_MODEL - default_value: /models/ootb/ei/yolo-x-nano.eim - description: path to the model file - - name: VIDEO_DEVICE - default_value: /dev/video1 diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/docs/arduino/arduino_cloud/README.md b/internal/orchestrator/bricks/testdata/assets/0.4.8/docs/arduino/arduino_cloud/README.md deleted file mode 100644 index 00e3ab98..00000000 --- a/internal/orchestrator/bricks/testdata/assets/0.4.8/docs/arduino/arduino_cloud/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Arduino Cloud Brick - -This Brick provides integration with the Arduino Cloud platform, enabling IoT devices to communicate and synchronize data seamlessly. - -## Overview - -The Arduino Cloud Brick simplifies the process of connecting your Arduino device to the Arduino Cloud. It abstracts the complexities of device management, authentication, and data synchronization, allowing developers to focus on building applications and features. With this module, you can easily register devices, exchange data, and leverage cloud-based automation for your projects. - -## Features - -- Connects Arduino devices to the Arduino Cloud -- Supports device registration and authentication -- Enables data exchange between devices and the cloud -- Provides APIs for sending and receiving data - -## Prerequisites - -To use this Brick, we need to have an active Arduino Cloud account, and a **device** and **thing** setup. To obtain the credentials, please follow the instructions at this [link](https://docs.arduino.cc/arduino-cloud/features/manual-device/). This is also covered in the [Blinking LED with Arduino Cloud](/examples/cloud-blink). - -During the device configuration, we will obtain a `device_id` and `secret_key`, which is needed to use this Brick. Note that a Thing with the device associated is required, and that you will need to create variables / dashboard to send and receive data from the board. - -### Adding Credentials - -The `device_id` and `secret_key` can be added inside the Arduino Cloud brick, by clicking on the **Brick Configuration** button inside the Brick. - -Clicking the button will provide two fields where the `device_id` and `secret_key` can be added to the Brick. - -## Code Example and Usage - -```python -from arduino.app_bricks.arduino_cloud import ArduinoCloud -from arduino.app_utils import App, Bridge - -iot_cloud = ArduinoCloud() - -def led_callback(client: object, value: bool): - """Callback function to handle LED blink updates from cloud.""" - print(f"LED blink value updated from cloud: {value}") - Bridge.call("set_led_state", value) - -iot_cloud.register("led", value=False, on_write=led_callback) - -App.run() -``` \ No newline at end of file diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/1_led_blink.py b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/1_led_blink.py deleted file mode 100644 index 58cd9470..00000000 --- a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/1_led_blink.py +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA -# -# SPDX-License-Identifier: MPL-2.0 - -# EXAMPLE_NAME = "Arduino Cloud LED Blink Example" -from arduino.app_bricks.arduino_cloud import ArduinoCloud -from arduino.app_utils import App -import time - -# If secrets are not provided in the class initialization, they will be read from environment variables -arduino_cloud = ArduinoCloud() - - -def led_callback(client: object, value: bool): - """Callback function to handle LED blink updates from cloud.""" - print(f"LED blink value updated from cloud: {value}") - - -arduino_cloud.register("led", value=False, on_write=led_callback) - -App.start_brick(arduino_cloud) -while True: - arduino_cloud.led = not arduino_cloud.led - print(f"LED blink set to: {arduino_cloud.led}") - time.sleep(3) diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/2_light_with_colors_monitor.py b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/2_light_with_colors_monitor.py deleted file mode 100644 index 40371db7..00000000 --- a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/2_light_with_colors_monitor.py +++ /dev/null @@ -1,21 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA -# -# SPDX-License-Identifier: MPL-2.0 - -# EXAMPLE_NAME = "Arduino Cloud Light with Colors Example" -from arduino.app_bricks.arduino_cloud import ArduinoCloud, ColoredLight -from arduino.app_utils import App -from typing import Any - -# If secrets are not provided in the class initialization, they will be read from environment variables -arduino_cloud = ArduinoCloud() - - -def light_callback(client: object, value: Any): - """Callback function to handle light updates from cloud.""" - print(f"Light value updated from cloud: {value}") - - -arduino_cloud.register(ColoredLight("clight", swi=True, on_write=light_callback)) - -App.run() diff --git a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/3_light_with_colors_command.py b/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/3_light_with_colors_command.py deleted file mode 100644 index 5e730ea9..00000000 --- a/internal/orchestrator/bricks/testdata/assets/0.4.8/examples/arduino/arduino_cloud/3_light_with_colors_command.py +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA -# -# SPDX-License-Identifier: MPL-2.0 - -# EXAMPLE_NAME = "Arduino Cloud Light with Colors Example" -from arduino.app_bricks.arduino_cloud import ArduinoCloud, ColoredLight -from arduino.app_utils import App -from typing import Any -import time -import random - -# If secrets are not provided in the class initialization, they will be read from environment variables -arduino_cloud = ArduinoCloud() - - -def light_callback(client: object, value: Any): - """Callback function to handle light updates from cloud.""" - print(f"Light value updated from cloud: {value}") - - -arduino_cloud.register(ColoredLight("clight", swi=True, on_write=light_callback)) - -App.start_brick(arduino_cloud) - -while True: - # randomize color - arduino_cloud.clight.hue = random.randint(0, 360) - arduino_cloud.clight.sat = random.randint(0, 100) - arduino_cloud.clight.bri = random.randint(0, 100) - print(f"Light set to hue: {arduino_cloud.clight.hue}, saturation: {arduino_cloud.clight.sat}, brightness: {arduino_cloud.clight.bri}") - time.sleep(3) diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/README.md b/internal/orchestrator/bricks/testdata/examples/cloud-blink/README.md deleted file mode 100644 index a20d5a09..00000000 --- a/internal/orchestrator/bricks/testdata/examples/cloud-blink/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Blinking LED from Arduino Cloud - -This **Blinking LED from Arduino Cloud** example allows us to remotely control the onboard LED on the Arduino® UNO Q from the [Arduino Cloud](https://app.arduino.cc/). -The LED is controlled by creating a dashboard with a switch in the Arduino Cloud. - -## Bricks Used - -The Blinking LED from Arduino Cloud example uses the following Bricks: - -- `arduino_cloud`: Brick to create a connection to the Arduino Cloud - -## Hardware and Software Requirements - -### Hardware - -- [Arduino® UNO Q](https://store.arduino.cc/products/uno-q) -- [USB-C® cable](https://store.arduino.cc/products/usb-cable2in1-type-c) - -### Software - -- Arduino App Lab -- [Arduino Cloud](https://app.arduino.cc/) - -**Note:** You can run this example using your Arduino® UNO Q as a Single Board Computer (SBC preview mode) using a [USB-C® hub](https://store.arduino.cc/products/usb-c-to-hdmi-multiport-adapter-with-ethernet-and-usb-hub) with a mouse, keyboard and display attached. - -## How to Use the Example - -This example requires an active Arduino Cloud account, with a device, thing and dashboard set up. - -### Setting Up Arduino Cloud - -1. Navigate to the [Arduino Cloud](https://app.arduino.cc/) page and log in / create an account. -2. Go to the [devices](https://app.arduino.cc/devices) page and create a device, selecting the "manual device" type. Follow the instructions and take note of the **device_id** and **secret_key** provided in the setup. - ![Arduino Cloud credentials](assets/docs_images/cloud-blink-device.png) -3. Go to the [things](https://app.arduino.cc/things) page and create a new thing. -4. Inside the thing, create a new **boolean** variable, and name it **"led"**. We also need to associate the device we created with this thing. - ![Arduino Cloud thing](assets/docs_images/cloud-blink-thing.png) -5. Finally, navigate to the [dashboards](), and create a dashboard. Inside the dashboard, click on **"Edit"**, and select the thing we just created. This will automatically assign a switch widget to the **led** variable. - ![Arduino Cloud dashboard](assets/docs_images/cloud-blink-dashboard.png) - -### Configure & Launch App - -1. Duplicate this example, by clicking on the arrow next to the App example name. As we will need to add the credentials, we will need to duplicate it, as we are not able to edit any of the built-in examples. - ![Duplicate example](assets/docs_images/cloud-blink-duplicate.png) - -2. In the Arduino App Lab on this page, go to `app.yaml` file, and add the **device_id** and **secret_key** credentials to the `ARDUINO_DEVICE_ID` and `ARDUINO_SECRET` variables. - ![Add credentials](assets/docs_images/cloud-blink-creds.png) - -3. Launch the App by clicking on the "Play" button in the top right corner. Wait until the App has launched. - ![Launching an App](assets/docs_images/launch-app-cloud-blink.png) - -## How it Works - -The application works by establishing a connection between the Arduino Cloud and the UNO Q board. When interacting with the dashboard's switch widget (turn ON/OFF), the cloud updates the "led" property. - -The `main.py` script running on the Linux system listens for changes to this property using the `arduino_cloud` Brick. When a change is detected, the **Bridge** tool is used to send data to the microcontroller, and turn the LED ON. - -The flow of the App is: -1. The switch in the Arduino Cloud dashboard is changed. -2. The Arduino Cloud updates the device's state. -3. `main.py` receives the updated state, sends a message to the microcontroller which turns the LED to an ON/OFF state. - -![How the Cloud interacts with UNO Q](assets/docs_images/cloud-blink.png) - -### Understanding the Code - -On the Linux (Python®) side: -- `iot_cloud = ArduinoCloud()` - initializes the `ArduinoCloud` class. -- `iot_cloud.register("led", value=False, on_write=led_callback)` - creates a callback function that fires when the value in the Arduino Cloud changes. -- `Bridge.call("set_led_state", value)` - calls the microcontroller with the updated state. - -On the microcontroller (sketch) side: -- `Bridge.provide("set_led_state", set_led_state);` - we receive an update from the Linux (Python®) side, and trigger the `set_led_state()` function. - -The `set_led_state()` function passes the updated state, and turns ON/OFF the LED: - -```arduino -void set_led_state(bool state) { - digitalWrite(LED_BUILTIN, state ? LOW : HIGH); -} -``` diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/app.yaml b/internal/orchestrator/bricks/testdata/examples/cloud-blink/app.yaml deleted file mode 100644 index 8af8e098..00000000 --- a/internal/orchestrator/bricks/testdata/examples/cloud-blink/app.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: Blinking LED from Arduino Cloud -icon: ☁️ -description: Control the LED from the Arduino IoT Cloud using RPC calls - -bricks: - - arduino:arduino_cloud \ No newline at end of file diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/python/main.py b/internal/orchestrator/bricks/testdata/examples/cloud-blink/python/main.py deleted file mode 100644 index f5264020..00000000 --- a/internal/orchestrator/bricks/testdata/examples/cloud-blink/python/main.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA -# -# SPDX-License-Identifier: MPL-2.0 - -# EXAMPLE_NAME = "Arduino Cloud LED Blink Example" -from arduino.app_bricks.arduino_cloud import ArduinoCloud -from arduino.app_utils import App, Bridge - -# If secrets are not provided in the class initialization, they will be read from environment variables -iot_cloud = ArduinoCloud() - - -def led_callback(client: object, value: bool): - """Callback function to handle LED blink updates from cloud.""" - print(f"LED blink value updated from cloud: {value}") - # Call a function in the sketch, using the Bridge helper library, to control the state of the LED connected to the microcontroller. - # This performs a RPC call and allows the Python code and the Sketch code to communicate. - Bridge.call("set_led_state", value) - -iot_cloud.register("led", value=False, on_write=led_callback) - -App.run() diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.ino b/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.ino deleted file mode 100644 index 8c7aef66..00000000 --- a/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.ino +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA -// -// SPDX-License-Identifier: MPL-2.0 - -#include - -void setup() { - pinMode(LED_BUILTIN, OUTPUT); - - Bridge.begin(); - Bridge.provide("set_led_state", set_led_state); -} - -void loop() {} - -void set_led_state(bool state) { - // LOW state means LED is ON - digitalWrite(LED_BUILTIN, state ? LOW : HIGH); -} diff --git a/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.yaml b/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.yaml deleted file mode 100644 index d9fe917e..00000000 --- a/internal/orchestrator/bricks/testdata/examples/cloud-blink/sketch/sketch.yaml +++ /dev/null @@ -1,11 +0,0 @@ -profiles: - default: - fqbn: arduino:zephyr:unoq - platforms: - - platform: arduino:zephyr - libraries: - - MsgPack (0.4.2) - - DebugLog (0.8.4) - - ArxContainer (0.7.0) - - ArxTypeTraits (0.3.1) -default_profile: default From d261782151ebb11b343dbfe660ae53d7410c624d Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Thu, 30 Oct 2025 17:20:28 +0100 Subject: [PATCH 5/8] refactoring --- internal/orchestrator/bricks/bricks.go | 31 +++++++------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index ac8526ba..50318ac7 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -163,16 +163,12 @@ func (s *Service) BricksDetails(id string, idProvider *app.IDProvider, } }) - appList, err := getAppList(cfg) - if err != nil { - slog.Error("unable to get app list", slog.String("error", err.Error())) - return BrickDetailsResult{}, fmt.Errorf("unable to get app list: %w", err) - } - usedByApps, err := getUsedByApps(appList, brick.ID, idProvider) + usedByApps, err := getUsedByApps(cfg, brick.ID, idProvider) if err != nil { slog.Error("unable to get used by apps", slog.String("error", err.Error())) return BrickDetailsResult{}, fmt.Errorf("unable to get used by apps: %w", err) } + return BrickDetailsResult{ ID: id, Name: brick.Name, @@ -188,16 +184,15 @@ func (s *Service) BricksDetails(id string, idProvider *app.IDProvider, }, nil } -func getAppList( - cfg config.Configuration, -) ([]app.ArduinoApp, error) { +func getUsedByApps( + cfg config.Configuration, brickId string, idProvider *app.IDProvider) ([]AppReference, error) { var ( pathsToExplore paths.PathList appPaths paths.PathList ) pathsToExplore.Add(cfg.ExamplesDir()) pathsToExplore.Add(cfg.AppsDir()) - arduinoApps := []app.ArduinoApp{} + usedByApps := []AppReference{} for _, p := range pathsToExplore { res, err := p.ReadDirRecursiveFiltered(func(file *paths.Path) bool { @@ -212,7 +207,7 @@ func getAppList( if err != nil { slog.Error("unable to list apps", slog.String("error", err.Error())) - return arduinoApps, err + return usedByApps, err } appPaths.AddAllMissing(res) } @@ -220,21 +215,11 @@ func getAppList( for _, file := range appPaths { app, err := app.Load(file.String()) if err != nil { - /* result.BrokenApps = append(result.BrokenApps, orchestrator.BrokenAppInfo{ - Name: file.Base(), - Error: fmt.Sprintf("unable to parse the app.yaml: %s", err.Error()), - })*/ + //we are not considering the borken apps + slog.Warn("unable to parse app.yaml, skipping", "path", file.String(), "error", err.Error()) continue } - arduinoApps = append(arduinoApps, app) - } - return arduinoApps, nil -} - -func getUsedByApps(apps []app.ArduinoApp, brickId string, idProvider *app.IDProvider) ([]AppReference, error) { - usedByApps := []AppReference{} - for _, app := range apps { for _, b := range app.Descriptor.Bricks { if b.ID == brickId { id, err := idProvider.IDFromPath(app.FullPath) From 78bf6f31dfa9e77c429ddc8542879b466d1b7862 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Thu, 30 Oct 2025 17:35:37 +0100 Subject: [PATCH 6/8] make lint happy --- internal/orchestrator/bricks/bricks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index 50318ac7..c1adff0c 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -215,7 +215,7 @@ func getUsedByApps( for _, file := range appPaths { app, err := app.Load(file.String()) if err != nil { - //we are not considering the borken apps + // we are not considering the borken apps slog.Warn("unable to parse app.yaml, skipping", "path", file.String(), "error", err.Error()) continue } From b4dad504ee605824d7857a31b0f249d400ba8059 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 3 Nov 2025 14:55:31 +0100 Subject: [PATCH 7/8] code review fixes --- internal/e2e/daemon/brick_test.go | 4 ++-- internal/orchestrator/bricks/bricks.go | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/e2e/daemon/brick_test.go b/internal/e2e/daemon/brick_test.go index 5f0c1eba..fa1cab40 100644 --- a/internal/e2e/daemon/brick_test.go +++ b/internal/e2e/daemon/brick_test.go @@ -33,7 +33,7 @@ import ( "github.com/arduino/arduino-app-cli/internal/store" ) -func setupTesBrick(t *testing.T) (*client.CreateAppResp, *client.ClientWithResponses) { +func setupTestBrick(t *testing.T) (*client.CreateAppResp, *client.ClientWithResponses) { httpClient := GetHttpclient(t) createResp, err := httpClient.CreateAppWithResponse( t.Context(), @@ -87,7 +87,7 @@ func TestBricksList(t *testing.T) { } func TestBricksDetails(t *testing.T) { - _, httpClient := setupTesBrick(t) + _, httpClient := setupTestBrick(t) t.Run("should return 404 Not Found for an invalid brick ID", func(t *testing.T) { invalidBrickID := "notvalidBrickId" diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index c1adff0c..15fcf6a6 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -165,7 +165,6 @@ func (s *Service) BricksDetails(id string, idProvider *app.IDProvider, usedByApps, err := getUsedByApps(cfg, brick.ID, idProvider) if err != nil { - slog.Error("unable to get used by apps", slog.String("error", err.Error())) return BrickDetailsResult{}, fmt.Errorf("unable to get used by apps: %w", err) } @@ -204,7 +203,6 @@ func getUsedByApps( } return false }, paths.FilterDirectories(), paths.FilterOutNames("python", "sketch", ".cache")) - if err != nil { slog.Error("unable to list apps", slog.String("error", err.Error())) return usedByApps, err @@ -215,7 +213,7 @@ func getUsedByApps( for _, file := range appPaths { app, err := app.Load(file.String()) if err != nil { - // we are not considering the borken apps + // we are not considering the broken apps slog.Warn("unable to parse app.yaml, skipping", "path", file.String(), "error", err.Error()) continue } From de2b4ad5360b9518d5ca2f21105a81ee171b4263 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 3 Nov 2025 16:19:34 +0100 Subject: [PATCH 8/8] fix error message --- internal/orchestrator/bricks/bricks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index 15fcf6a6..759b5cc6 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -222,7 +222,7 @@ func getUsedByApps( if b.ID == brickId { id, err := idProvider.IDFromPath(app.FullPath) if err != nil { - return usedByApps, fmt.Errorf("failed to get app ID for %s: %w", app.Name, err) + return usedByApps, fmt.Errorf("failed to get app ID for %s: %w", app.FullPath, err) } usedByApps = append(usedByApps, AppReference{ Name: app.Name,